From 66c56ece9ec2a616c962280d35f8a4252337e0f5 Mon Sep 17 00:00:00 2001 From: KooshaPari <42529354+KooshaPari@users.noreply.github.com> Date: Thu, 16 Jul 2026 23:04:00 -0700 Subject: [PATCH] Add cliproxy provider exposure controls and manifest injection (#7329) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(fusion): let judge use its own knowledge and override the panel (#6804) The judge prompt said to write an answer 'grounded in that analysis', implicitly capping output at the panel's union. When all panel members miss or are collectively wrong on something, the judge should apply its own reasoning as a full participant and override consensus, while keeping an honesty guard against fabrication. Adds a regression test. Co-authored-by: Chirag Singhal * fix(api): raise provider apiKey cap for cookie-based web providers (#6715) (#6759) * fix(cli): fall back to settings.json when Claude Code binary is unresolvable (#6701) (#6734) getCliRuntimeStatus() only ever answered `installed` from binary resolution (known install paths + where/which PATH search), so a stale PATH, moved binary, or uncatalogued install method reported "not found" even when ~/.claude/settings.json proved the CLI was installed and used before — regressing behind upstream 9router's checkClaudeInstalled(), which already falls back to the settings file when where/which fails. withSettingsFallback() (new src/shared/services/cliInstallFallback.ts, kept out of the frozen cliRuntime.ts to respect its file-size ceiling) restores that parity: only when the binary lookup's own reason is "not_found" (never for deliberate security rejections like unsafe/relative env overrides or symlink escapes) and the tool's settings file exists on disk. * fix(providers): honor explicit thinking.budget_tokens 0 in openai->gemini transform (#6813) (#6821) The transform forwarded the Claude-style thinking.budget_tokens into generationConfig.thinkingConfig.thinkingBudget, but the presence check was truthy (&& thinking.budget_tokens). An explicit budget_tokens: 0 — the natural way to disable thinking — is falsy, so it was dropped and the request fell through to the default thinkingConfig injection, making the model think despite an explicit request for zero. Use an explicit numeric check so 0 is honored as thinkingBudget 0; includeThoughts is only set for a non-zero budget. * fix(compression): reconcile outer vs per-engine token counts (#6488) (#6741) * fix(compression): reconcile outer vs per-engine token counts on degenerate output (#6488) Outer originalTokens/compressedTokens (real tiktoken counter over extracted message text) diverged from engineBreakdown[0]'s counts (a crude JSON.stringify(requestBody).length/4 estimate), worst on small/degenerate inputs where JSON structural overhead dominates. A single-engine breakdown entry represents the exact same before/after transformation as the overall response, so reconcileSingleEngineTokens() now overwrites that one entry's counts with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched. * chore(6741): resolve release sync — CHANGELOG.md restored to release tip, entry moved to changelog.d fragment (fragments-first) * fix(api): accept enableRenderers in RTK compression config schema (#6703) (#6757) * fix(db): break probe-failed/restore loop on large storage.sqlite (#6632) Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy files belong to #6620, not this PR); keeps only the author's changes. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * feat(cursor): add Opus 4.8, Fable 5, and Sonnet 5 model families (#6779) Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy files belong to #6620, not this PR); keeps only the author's cursor registry + test changes. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(translator): read PDF/video file attachments for Gemini/Antigravity and Claude (#6790) Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy files belong to #6620, not this PR); keeps only the author's translator + test changes. Co-authored-by: Wital Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(codex): strip include from compact responses requests (#6805) * fix(codex): strip include from compact responses requests Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy files belong to #6620, not this PR); keeps only the author's changes. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(6805): move include-strip assertion to standalone test file to keep executor-codex.test.ts under frozen size cap Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6769) Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy files belong to #6620, not this PR); keeps only the author's changes. Co-authored-by: Chirag Singhal Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(bootstrap): filter empty process.env values to prevent Docker env crash loop (#6828) Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy files belong to #6620, not this PR) and the direct CHANGELOG.md edit (fragments-first); keeps only the author's bootstrap change. Co-authored-by: Andrian B. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(providers): update SenseNova Token Plan support (#6330) Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy files belong to #6620, not this PR); the author's constants/registry/snapshot deltas were re-applied cleanly onto the release tip. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(providers): classify 404 as MODEL_NOT_FOUND to stop retry storm (#6829) Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy files belong to #6620, not this PR) and the direct CHANGELOG.md edit (fragments-first); the author's chatCore/errorClassifier deltas were re-applied cleanly onto the release tip. Co-authored-by: Andrian B. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(api): accept all catalog engines on compression PUT schema (#6792) Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy files belong to #6620, not this PR). Resolved the release's OmniGlyph engine addition additively (types.ts/compression.ts kept both 'relevance' and 'omniglyph') and extended stackedPipelineStepSchema + STACKED_PIPELINE_ENGINE_INTENSITIES with the omniglyph branch so the ENGINE_CATALOG-parity test passes. Co-authored-by: Pitchfork-and-Torch Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(api): point CLI health command at /api/monitoring/health (#6677) (#6717) * fix(api): point CLI health command at /api/monitoring/health (#6677) bin/cli/commands/health.mjs called GET /api/health, a route that was moved to /api/monitoring/health without updating the CLI; the top-level /api/health handler never existed on disk (only degradation/ and ping/ sub-routes). Point runHealthCommand()/runHealthComponentsCommand() at /api/monitoring/health and read its real payload shape (activeConnections, circuitBreakers: {open,halfOpen,closed}, memoryUsage) instead of the old nonexistent requests/breakers/cache/memory fields. * chore(6717): re-sync onto release tip; move CHANGELOG entry to changelog.d fragment (fragments-first) * chore(cursor): add Grok 4.5 effort/fast model IDs (#6774) Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy files belong to #6620, not this PR); keeps only the author's changes. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED (#6791) * fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy files belong to #6620, not this PR); keeps only the author's changes. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * refactor(deepseek): extract done-terminator helper to keep frozen file under cap Extracts the FINISHED-drain scheduler and finish-once guard added for the [DONE] terminator fix (#6777) into a new deepseek-web-done-terminator.ts module, so deepseek-web.ts stays under its frozen line cap (1148). Behavior is unchanged. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Pitchfork-and-Torch Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * feat(models): add capability override UI (#6727) * feat(models): add capability override UI Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy files belong to #6620, not this PR); renumbered the migration 118 -> 119 to resolve the collision with 118_provider_param_filters.sql already on release/v3.8.47; the author's i18n/localDb deltas were re-applied cleanly onto the release tip. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(6727): import model-capability-overrides DB fns directly (not via localDb barrel) to keep localDb under file-size cap; aligns with anti-barrel convention * chore(db): satisfy known-symbols contract for modelCapabilityOverrides Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza * fix(cursor): use Agent CLI build id for x-cursor-client-version (#6795) * fix(cursor): use Agent CLI build id for x-cursor-client-version Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy files belong to #6620, not this PR); the author's .env.example/docs deltas were re-applied cleanly onto the release tip. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * chore(changelog): re-sync CHANGELOG.md to release tip (restore #6701 bullet) Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(startup): rename reasoningControls.ts to avoid webpack casing collision (#6584) (#6718) * fix(startup): rename reasoningControls.ts to avoid webpack casing collision (#6584) * chore(6718): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first) * fix(build): suppress Turbopack over-bundling warning from agentSkills generator (#6582) (#6720) * fix(build): suppress Turbopack over-bundling warning from agentSkills generator (#6582) generator.ts builds outputBase from a non-literal outputDir parameter, so Turbopack's file-tracing analyzer can't narrow it and emits an "Overly broad patterns" warning per entry point that imports the module (603 warnings on v3.8.46, up from 379). The fs access is legitimate and bounded, so next.config.mjs now suppresses this specific diagnostic via turbopack.ignoreIssue, mirroring the existing webpack.ignoreWarnings precedent in the same file. * chore(6720): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first) * fix(providers): drop image_generation for Codex Spark models regardless of plan (#6651) (#6721) * fix(providers): drop image_generation for Codex Spark models regardless of plan (#6651) * chore(6721): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first) * fix(providers): stop quota card re-sorting Codex/GLM bars by remaining % (#6687) (#6722) * fix(providers): stop quota card re-sorting Codex/GLM bars by remaining % (#6687) QuotaCardExpanded.tsx unconditionally re-sorted quotas by remaining percentage via sortQuotasByRemaining(), discarding the deterministic CODEX_QUOTA_ORDER/GLM_QUOTA_ORDER window order quotaParsing.ts's sortCodexOrder()/sortGlmOrder() had already established. A new hasFixedQuotaOrder() + resolveQuotaDisplayOrder() skip the re-sort for providers with a fixed window order (codex, glm family), threading providerId from QuotaCard.tsx through to the display layer. Regression guard: tests/unit/quota-card-expanded-fixed-order-6687.test.ts * chore(6722): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first) * fix(startup): lazy-import ioredis in rateLimiter to fix MCP ERR_MODULE_NOT_FOUND (#6559) (#6725) * fix(startup): lazy-import ioredis in rateLimiter to fix MCP ERR_MODULE_NOT_FOUND (#6559) * chore(6725): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first) * fix(resilience): resolve fp-pinned combo account back to real connection id (#6696) (#6732) * fix(resilience): resolve fp-pinned combo account back to real connection id (#6696) * chore(6732): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first) * fix(api): Responses passthrough emits event-only SSE frames after filtering commentary output (#6561) (#6735) * fix(api): Responses passthrough emits event-only SSE frames after filtering commentary output (#6561) The #6199 commentary-drop `continue;` branches in stream.ts skipped the data: line for a dropped commentary event but never cleared the already-buffered event: line for the same frame, so the next blank line flushed the stale event: line alone -- an event-only SSE frame that crashes the OpenAI Python SDK's json.loads(). Both drop sites now call clearPendingPassthroughEvent() before continue. The commentary-drop decision was extracted into a new responsesCommentaryDrop.ts module so the fix does not grow the frozen stream.ts. * chore(6735): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first) * fix(api): emit reasoning_content on claude-web + v0-vercel-web SSE (#6662) (#6743) * fix(api): emit reasoning_content on claude-web + v0-vercel-web /v1/chat/completions SSE (#6662) * chore(6743): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first) * fix(sse): unwrap bare {function:{…}} tools in openai→claude translation (#6704) * fix(sse): unwrap bare {function:{…}} tools in openai→claude translation Some OpenAI-shape clients send a tool as a bare `{ function: {...} }` object, omitting the spec-required `type: "function"` parent wrapper. The tools-mapping in openai-to-claude.ts (~line 366) only unwrapped `tool.function` when `tool.type === "function"` was ALSO true, so a bare-function tool fell through to `toolData = tool` (the wrapper itself, with no `.name`), producing an empty `originalName` and silently dropping the tool from the translated request — worse than a 400, since the caller has no signal the tool never made it upstream. Unwrap `tool.function` whenever present, independent of the parent `type` field. Regression guard: tests/unit/openai-to-claude-bare-tool.test.ts. Co-authored-by: Samir Abis Inspired-by: https://github.com/decolua/9router/pull/2473 * chore(6704): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first) --------- Co-authored-by: Samir Abis * fix(oauth): avoid bare-email dedup of Codex OAuth logins (#6706) * fix(oauth): avoid bare-email dedup of Codex OAuth logins When an incoming Codex OAuth connection has no verifiable workspace/account id, do not merge it into an existing row on email match alone — that silently overwrote the other account's token pair. Require a matching chatgptUserId (a stable per-account JWT id) before merging; otherwise insert a distinct connection row. Co-authored-by: lucasjustinudin <34107354+lucasjustinudin@users.noreply.github.com> Inspired-by: https://github.com/decolua/9router/pull/2477 * chore(6706): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first) --------- Co-authored-by: lucasjustinudin <34107354+lucasjustinudin@users.noreply.github.com> * fix(sse): skip thinkingConfig for gemma models in openai→gemini translation (#6708) open-sse/translator/request/claude-to-gemini.ts already guards against sending thinkingConfig for gemma-4-* models (Gemma doesn't support it — Vertex returns 400: "Thinking budget is not supported for this model"), but the OpenAI-shape path (openai-to-gemini.ts) lacked the same guard, so OpenAI-shape clients hitting a vertex gemma-4-* model still got a 400. Mirrors the existing claude-to-gemini.ts guard: wrap the reasoning_effort and Claude-shape thinking.budget_tokens branches with a model.startsWith ("gemma-4") check. Branch 3 (default includeThoughts for modern Gemini models) already excludes non-"gemini" model ids and needed no change. Inspired-by: https://github.com/decolua/9router/pull/2480 Co-authored-by: chy1211 <31048289+chy1211@users.noreply.github.com> * fix(codex): surface capacity errors embedded in 200-OK SSE streams (#6710) * fix(codex): surface capacity errors embedded in 200-OK SSE streams Codex sometimes answers with HTTP 200 and a text/event-stream body whose payload carries a transient error mid-stream (e.g. "Selected model is at capacity...", server_is_overloaded, service_unavailable_error). Because the outer HTTP status was 200, this looked like a successful response to every caller — no retry, no circuit breaker, and no combo/account fallback ever engaged, so a healthy account sat idle while the request silently failed or truncated. Add peekCodexSseTransientError() to open-sse/executors/codex.ts: it peeks the first bytes of a text/event-stream Codex response, pattern-matches the known transient-error signatures, and converts a match into a real 503 Response via errorResponse() (Hard Rule #12 — sanitized, never raw upstream text). A 503 is already a recognized provider-failure status in accountFallback.ts, so combo routing and connection cooldown pick it up automatically. When no error signature is found, the peeked prefix is prepended back onto the remaining upstream body so the passthrough stays byte-identical to the unmodified response. Regression guard: tests/unit/codex-sse-capacity-fallback.test.ts — a model-at-capacity payload and a server_is_overloaded/service_unavailable_error payload both convert to 503; a normal single-chunk SSE stream and one split across multiple network chunks both reassemble byte-for-byte unchanged. Inspired-by: https://github.com/decolua/9router/pull/2452 (sub-bug #3 only — OmniRoute already covers PR #2452's other two sub-bugs: service_tier "fast" normalization and reasoning_effort "max" normalization). Co-authored-by: ryanngit <74137224+ryanngit@users.noreply.github.com> * chore(6710): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first) --------- Co-authored-by: ryanngit <74137224+ryanngit@users.noreply.github.com> * fix(volcengine): clamp Kimi max_tokens to Ark endpoint cap (#6712) * fix(volcengine): clamp Kimi max_tokens to Ark endpoint cap VolcEngine Ark's Kimi coding-plan endpoint (ark.cn-beijing.volces.com) enforces max_tokens <= 32768 server-side and returns 400 "integer above maximum value, expected a value <= 32768" for anything over that ceiling. OmniRoute's StripRule only supported dropping params outright, with no numeric clamp mechanism, so a client sending a larger max_tokens (common default, e.g. 65536) 400s outright against volcengine's kimi-k2-5-260127. The 32768 cap is independently confirmed against two live-endpoint bug reports hitting this exact Ark endpoint for both kimi-k2.5 and kimi-k2.7-code (NousResearch/hermes-agent#51773, MoonshotAI/kimi-cli#1124), not just upstream's own value — same cap upstream 9router#2460 uses. StripRule gains two optional fields: `clampToModelMaxOutput` (clamp to the model's own catalog maxOutputTokens ceiling, when set) and `maxOutputCap` (a fixed endpoint-imposed ceiling); when both apply, the lower wins. The new rule is scoped to the literal id `kimi-k2-5-260127` (OmniRoute's real volcengine Kimi model, not upstream's `Kimi-K2.7-Code`), not a broad /kimi/i regex, so it can never clamp an unrelated future Kimi listing whose Ark cap may differ. glm-4-7-251222 (the other volcengine model) is unaffected. Inspired-by: https://github.com/decolua/9router/pull/2460 Co-authored-by: whale9820 * chore(6712): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first) --------- Co-authored-by: whale9820 * fix(antigravity): surface aborted Gemini tool calls off end_turn (#6713) * fix(antigravity): surface aborted Gemini tool calls off end_turn Gemini/Antigravity aborts a turn with finishReason MALFORMED_FUNCTION_CALL (or a sibling like UNEXPECTED_TOOL_CALL) instead of completing cleanly. Both Claude-facing translators collapsed these to a clean end_turn, hiding the aborted tool call as a successful completion: - the OpenAI hub path (openai-to-claude.ts convertFinishReason default), and - the DIRECT Gemini->Claude path (gemini-to-claude.ts), which is the one Claude Code actually hits through an antigravity/Gemini-routed model. Add isAbortFinishReason() to finishReason.ts and map these reasons to tool_use on both paths; genuinely unknown reasons still fall back to end_turn. Co-authored-by: anhdiepmmk Inspired-by: https://github.com/decolua/9router/pull/2462 * chore(6713): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first) --------- Co-authored-by: anhdiepmmk * fix(translator): strip empty cloud_base_branch from Cursor Subagent tool call (#6729) * fix(translator): strip empty cloud_base_branch from Cursor Subagent tool call (port from 9router#2446) The Responses->Chat tool-arg cleanup (stripEmptyOptionalToolArgs) only stripped empty-string/empty-array optional args for Claude Code's Read tool. Cursor's local Subagent tool call therefore passed through with the cloud-only field cloud_base_branch: "", which Cursor rejects ("cloud_base_branch may only be specified when environment equals cloud") before starting the subagent. Extend the cleanup to an allowlist of Read + Subagent; arbitrary tools stay untouched. Reported-by: like3213934360-lab (https://github.com/decolua/9router/issues/2446) * chore(6729): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first) * fix(translator): defer content_block_start until GLM streams the tool name (#6730) * fix(translator): defer content_block_start until GLM streams the tool name (port from 9router#2077) GLM 5.2 (and similar OpenAI-compatible upstreams) stream a tool call's id and function.name across separate SSE delta chunks. The openai-to-claude streaming translator emitted content_block_start immediately on the id-only chunk with an empty name; the Claude SSE protocol cannot patch a block after emission, so the later name-only chunk was dropped and Claude Code rejected the tool_use with an empty tool name / "No such tool available:". Defer content_block_start until the name arrives (start on args if they arrive first), and emit a start for any orphaned id-only tool call at finish so content_block_stop is never orphaned. Reported-by: itiwant (https://github.com/decolua/9router/issues/2077) * chore(6730): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first) * feat(dashboard): add search to Playground model picker dropdown (#4086) (#6811) * feat(dashboard): add search to Playground model picker dropdown (#4086) The shared ModelSelectModal (combo builder + CLI-code cards) already had search, but the Playground's raw model ` (#4086) — the shared `ModelSelectModal` (combo builder + CLI-code cards) already had search, but Playground's `StudioConfigPane` model dropdown stayed a flat unsearchable list, unusable once a provider like OpenRouter contributed 50+ models. Typing now filters the dropdown (Turkish-safe accent/case-insensitive match via `matchesSearch`), while the currently selected model always stays pinned in the list even if it no longer matches the query, so typing never silently swaps the active selection. Reuses the existing `common.search` i18n key (already translated in all 42 locales) — no new translation key needed. Regression guard: `tests/unit/playground-model-selection-3731.test.ts` (`filterModelsByQuery`), `tests/unit/ui/playground-model-search-4086.test.tsx`. diff --git a/changelog.d/features/6818-antigravity-weekly-quota.md b/changelog.d/features/6818-antigravity-weekly-quota.md new file mode 100644 index 0000000000..84abe02d9d --- /dev/null +++ b/changelog.d/features/6818-antigravity-weekly-quota.md @@ -0,0 +1 @@ +- **feat(usage):** Antigravity/agy quota widget now surfaces the **weekly** window alongside the existing per-model 5-hour window ([#4017](https://github.com/diegosouzapw/OmniRoute/issues/4017)) — the weekly limit isn't part of the per-model `retrieveUserQuota` response the fetcher already calls; it only appears in a separate, undocumented `retrieveUserQuotaSummary` RPC that groups models into families ("Gemini Models", "Claude and GPT models") with one weekly bucket per family. A new `usage/antigravityWeeklyQuota.ts` leaf fetches that RPC (cached, best-effort — a failure or unavailable RPC never breaks the existing per-model quotas) and parses the weekly bucket per group into `gemini_weekly`/`claude_gpt_weekly` quota entries, merged into the same `quotas` map the widget already renders generically. Regression guard: `tests/unit/antigravity-weekly-quota-4017.test.ts` (bucket parsing, the alternate `quotaSummary`-nested envelope, and end-to-end merge via `getUsageForProvider`). diff --git a/changelog.d/features/6820-codex-effort-model-echo.md b/changelog.d/features/6820-codex-effort-model-echo.md new file mode 100644 index 0000000000..d9646f81a3 --- /dev/null +++ b/changelog.d/features/6820-codex-effort-model-echo.md @@ -0,0 +1 @@ +- **feat(codex):** Codex CLI compatibility shim — the Responses API `response.created`/`response.in_progress`/`response.completed` payloads now carry a `model` field (previously absent), and for Codex-CLI-originated requests it echoes the client-requested, effort-suffixed model id (e.g. `gpt-5.5-xhigh`) instead of the bare upstream id (`gpt-5.5`), so the Codex CLI status line/model button shows the active reasoning effort ([#3697](https://github.com/diegosouzapw/OmniRoute/issues/3697)). `openaiToOpenAIResponsesResponse` (`open-sse/translator/response/openai-responses.ts`) now threads the upstream model into the Responses event objects; a new `isCodexOriginatedHeaders()` (`open-sse/config/codexIdentity.ts`, reusing PR #3481's `originator`/User-Agent detection) makes chatCore's existing opt-in `echoRequestedModelName` (#1311) model-echo pipeline fire automatically for Codex clients regardless of the setting, detected by request headers so it still applies when `codex/gpt-5.5-xhigh` is routed through a combo to a non-codex upstream; `echoModelInObject`/`echoModelInSseLine` (`open-sse/services/responseModelEcho.ts`) now also rewrite the nested `response.model` field the Responses API uses. `/v1/models` still returns `models: []` for Codex (unchanged). Regression guard: `tests/unit/codex-effort-model-echo-3697.test.ts`. diff --git a/changelog.d/features/6823-zai-web-cookie-provider.md b/changelog.d/features/6823-zai-web-cookie-provider.md new file mode 100644 index 0000000000..57100fc05c --- /dev/null +++ b/changelog.d/features/6823-zai-web-cookie-provider.md @@ -0,0 +1 @@ +- **Z.ai Web (free web-session provider)**: new `zai-web` web-cookie provider drives the free chat.z.ai consumer chat UI via a pasted browser session cookie, distinct from the existing API-key `zai`/`glm`/`glm-cn`/`glmt` providers (`api.z.ai`) — modeled on the `doubao-web`/`venice-web` cookie executors and the pre-existing `chatglm-web` credential requirement/token-extraction entries. `ZaiWebExecutor` (`open-sse/executors/zai-web.ts`) posts to `chat.z.ai/api/chat/completions` with the cookie forwarded both as `Cookie` and as `Authorization: Bearer `, and normalizes both z.ai's internal `delta_content`/`phase` SSE envelope and a pass-through OpenAI-shaped `choices[].delta` frame into standard chat-completion chunks. Registered in `WEB_COOKIE_PROVIDERS`, `WEB_SESSION_CREDENTIAL_REQUIREMENTS`, the provider registry (`zai-web` entry, GLM-4.6/4.5/4.5V models), and `tokenExtractionConfig.ts` for in-app cookie capture. Regression guard: `tests/unit/executor-zai-web.test.ts` (16 tests — token extraction, frame parsing for both SSE shapes, streaming and non-streaming aggregation, error paths). (#4056) diff --git a/changelog.d/features/6838-headroom-gcf-v3.2-nested-flattening.md b/changelog.d/features/6838-headroom-gcf-v3.2-nested-flattening.md new file mode 100644 index 0000000000..5752b80a24 --- /dev/null +++ b/changelog.d/features/6838-headroom-gcf-v3.2-nested-flattening.md @@ -0,0 +1 @@ +- **feat(compression):** update the vendored **GCF** codec behind the **Headroom** engine to spec **v3.2 (nested flattening)** ([#6837](https://github.com/diegosouzapw/OmniRoute/issues/6837)). Homogeneous arrays whose rows carry nested objects/arrays now tabularize via `>`-prefixed path fields instead of a low-yield per-row fallback, so nested MCP tool-result rows (`meta:{...}`, `tags:[...]`) compact like flat rows. On representative shapes the update takes deeply-nested payloads the old codec left near-uncompressed from ~3% to ~32% vs JSON (k8s pods, `cl100k_base`), with shallow-nested rows seeing a small bump and flat arrays unchanged. Re-vendored from current gcf-typescript (zero runtime deps, MIT, SPDX-marked, generic-profile only); also folds in the `[N]:` inline-array quoting fix and canonical decimal formatting. Round-trip stays lossless (order-insensitive), and the decoder is hardened against prototype pollution (a `__proto__`/`constructor` path segment never mutates `Object.prototype`, and keys shadowing built-ins like `toString` now round-trip correctly instead of misparsing). Regression guard: `tests/unit/compression/headroom-smartcrusher.test.ts` (deep-nested + prototype-pollution cases). diff --git a/changelog.d/features/6862-gpt-5-6-providers.md b/changelog.d/features/6862-gpt-5-6-providers.md new file mode 100644 index 0000000000..695473c505 --- /dev/null +++ b/changelog.d/features/6862-gpt-5-6-providers.md @@ -0,0 +1,2 @@ +- **feat(providers):** Add GPT-5.6 support across OpenAI API, Codex, and ChatGPT Web, including Codex Max/Ultra efforts, VS Code metadata, Fast-tier credit accounting, curated live discovery, the Codex 0.144.1 client identity, and correct chat routing for models that also support image generation ([#6862](https://github.com/diegosouzapw/OmniRoute/pull/6862)) - thanks @backryun +- **chore(providers):** Align emitted Claude Code identity headers, bridge fingerprints, provider profiles, and documented defaults with claude-cli 2.1.207 ([#6862](https://github.com/diegosouzapw/OmniRoute/pull/6862)) - thanks @backryun diff --git a/changelog.d/features/homolog-e2e-suite.md b/changelog.d/features/homolog-e2e-suite.md new file mode 100644 index 0000000000..b2c4637d66 --- /dev/null +++ b/changelog.d/features/homolog-e2e-suite.md @@ -0,0 +1 @@ +- **Homologation suite**: new `npm run homolog` runs the full release-homologation battery against the deployed VPS — health/version parity, API + real SSE streaming with an ephemeral API key (created and revoked by the run), minimal-cost real-provider smoke (promptfoo generated from the live catalog), and a Playwright sweep that loads every dashboard route and exercises the API-key UI flow — emitting a unified CTRF report that backs the release STOP #2 checklist diff --git a/changelog.d/fixes/1037-vercel-relay-sso-protection-check.md b/changelog.d/fixes/1037-vercel-relay-sso-protection-check.md new file mode 100644 index 0000000000..e03d00dd02 --- /dev/null +++ b/changelog.d/fixes/1037-vercel-relay-sso-protection-check.md @@ -0,0 +1 @@ +- **fix(api):** Vercel Relay deploy now checks the Deployment Protection (SSO) PATCH response and surfaces `ssoProtectionWarning` when Vercel rejects it, instead of silently activating a relay that later returns an undiagnosed `403 Access denied`. (thanks @ricatix) diff --git a/changelog.d/fixes/1382-streaming-empty-content-block.md b/changelog.d/fixes/1382-streaming-empty-content-block.md new file mode 100644 index 0000000000..c224cd2991 --- /dev/null +++ b/changelog.d/fixes/1382-streaming-empty-content-block.md @@ -0,0 +1 @@ +- **fix(combo):** streaming Claude responses whose content block opens (`content_block_start`) and closes with no usable text/tool_use — a shape some upstreams return for tool-heavy requests on HTTP 200 — are now detected by `validateResponseQuality`'s SSE peek and trigger combo failover instead of being forwarded to the client as a silent empty completion (thanks @heishen6). diff --git a/changelog.d/fixes/1556-openai-regex-lookaround.md b/changelog.d/fixes/1556-openai-regex-lookaround.md new file mode 100644 index 0000000000..7045d0917d --- /dev/null +++ b/changelog.d/fixes/1556-openai-regex-lookaround.md @@ -0,0 +1 @@ +- **fix(codex):** strip regex `pattern` lookaround (lookahead/lookbehind) from tool JSON Schemas on the Codex/OpenAI native passthrough path — previously only the translated-request path coerced tool schemas, so a `pattern` like `^(?=.*@).+$` reached OpenAI unmodified and was rejected with `regex lookaround is not supported`. (thanks @evinjohnn) (#7100) diff --git a/changelog.d/fixes/1809-mitm-stop-dns-before-kill.md b/changelog.d/fixes/1809-mitm-stop-dns-before-kill.md new file mode 100644 index 0000000000..06e42e461c --- /dev/null +++ b/changelog.d/fixes/1809-mitm-stop-dns-before-kill.md @@ -0,0 +1 @@ +- **fix(cli):** `stopMitm()` now removes /etc/hosts DNS-spoof entries before killing the MITM server process, closing the window where a client's DNS still resolved a target host to `127.0.0.1` while nothing was listening there — the cause of `connect ECONNREFUSED 127.0.0.1:443` right after stopping the MITM proxy (thanks @dionisius95). diff --git a/changelog.d/fixes/1811-composer-space-sep.md b/changelog.d/fixes/1811-composer-space-sep.md new file mode 100644 index 0000000000..856fab9e17 --- /dev/null +++ b/changelog.d/fixes/1811-composer-space-sep.md @@ -0,0 +1 @@ +- **fix(sse):** Cursor Composer/Auto tool calls that separate the arg name and value with a space instead of a newline (e.g. `path /Users/.../test`) no longer produce empty-valued, malformed argument keys, fixing silent no-op Write/tool calls. (thanks @way-art) diff --git a/changelog.d/fixes/1904-custom-model-vision-toggle.md b/changelog.d/fixes/1904-custom-model-vision-toggle.md new file mode 100644 index 0000000000..85fd14aff1 --- /dev/null +++ b/changelog.d/fixes/1904-custom-model-vision-toggle.md @@ -0,0 +1 @@ +- **fix(dashboard):** the "Custom Models" add/edit form now has a "Vision capable" toggle so a custom OpenAI-compatible model can be manually flagged as vision-capable when the provider's discovery metadata doesn't report an image input modality (thanks @nguyenphi37) diff --git a/changelog.d/fixes/1905-fusion-panel-oom.md b/changelog.d/fixes/1905-fusion-panel-oom.md new file mode 100644 index 0000000000..68bc6dbfbc --- /dev/null +++ b/changelog.d/fixes/1905-fusion-panel-oom.md @@ -0,0 +1 @@ +- **fix(combos):** fusion combos now reject an oversized panel (>40 models by default, tunable via `fusionTuning.maxPanel`) with a clean 400 before fanning out, instead of buffering dozens of concurrent full responses in memory and OOM-crashing the whole container. (thanks @fontvu) diff --git a/changelog.d/fixes/2032-openai-compatible-check-404-warning.md b/changelog.d/fixes/2032-openai-compatible-check-404-warning.md new file mode 100644 index 0000000000..5933bf065f --- /dev/null +++ b/changelog.d/fixes/2032-openai-compatible-check-404-warning.md @@ -0,0 +1 @@ +- **fix(providers):** the OpenAI-compatible "Check" validation flow now surfaces a warning when the chat-completions probe returns `404` (e.g. `model_not_found`) instead of silently passing as `Valid` — a bogus/non-standard model id (Featherless/OpenRouter-style `vendor/model` typos) previously went undetected at Check time and only surfaced once a real request tripped the per-model lockout. (thanks @advane204f) diff --git a/changelog.d/fixes/2413-preserve-agent-headers.md b/changelog.d/fixes/2413-preserve-agent-headers.md new file mode 100644 index 0000000000..6353f5dd77 --- /dev/null +++ b/changelog.d/fixes/2413-preserve-agent-headers.md @@ -0,0 +1 @@ +- **fix(executors):** forward agent-supplied `X-Session-ID`/`X-Title` metadata headers to upstream providers — previously dropped for every client outside the `x-opencode-*` allowlist. (thanks @chitholian) (#7104) diff --git a/changelog.d/fixes/2461-antigravity-streaming-403-raw-bytes.md b/changelog.d/fixes/2461-antigravity-streaming-403-raw-bytes.md new file mode 100644 index 0000000000..84cf50b3de --- /dev/null +++ b/changelog.d/fixes/2461-antigravity-streaming-403-raw-bytes.md @@ -0,0 +1 @@ +- **fix(sse):** Antigravity streaming requests that hit a non-ok upstream response (e.g. a 403) no longer pipe the raw upstream bytes straight through to the client — a binary/non-UTF8 error body (observed as gzip-magic-byte garbage) is now routed through the same sanitized `buildAntigravityUpstreamError()` path the non-streaming branch already used, instead of corrupting the client-visible error message. Regression guard: `tests/unit/antigravity-streaming-error-body-sanitized-2461.test.ts` — thanks @Duongkhanhtool diff --git a/changelog.d/fixes/2493-better-sqlite3-abi-validation.md b/changelog.d/fixes/2493-better-sqlite3-abi-validation.md new file mode 100644 index 0000000000..dba246809d --- /dev/null +++ b/changelog.d/fixes/2493-better-sqlite3-abi-validation.md @@ -0,0 +1 @@ +- **fix(cli):** the runtime self-heal now verifies a cached `better-sqlite3` native binary actually loads for the running Node before trusting it — the old check only inspected the file's magic bytes (ELF/Mach-O/PE header), so a binary built for a different Node ABI passed validation and segfaulted the process on first use instead of triggering a rebuild. (thanks @mrprohack) (#7105) diff --git a/changelog.d/fixes/5716-proxy-pool-relayauth-dropped.md b/changelog.d/fixes/5716-proxy-pool-relayauth-dropped.md new file mode 100644 index 0000000000..72d11d1f3a --- /dev/null +++ b/changelog.d/fixes/5716-proxy-pool-relayauth-dropped.md @@ -0,0 +1 @@ +- fix(providers): preserve relayAuth for vercel/deno/cloudflare relay proxies referenced by-id from the no-auth-provider Proxy Pool dropdown (#5716) diff --git a/changelog.d/fixes/6142-6142-devin-unified-api.md b/changelog.d/fixes/6142-6142-devin-unified-api.md new file mode 100644 index 0000000000..bda88bd4c8 --- /dev/null +++ b/changelog.d/fixes/6142-6142-devin-unified-api.md @@ -0,0 +1 @@ +- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) diff --git a/changelog.d/fixes/6272-6272-mimo-proxy.md b/changelog.d/fixes/6272-6272-mimo-proxy.md new file mode 100644 index 0000000000..37b11868be --- /dev/null +++ b/changelog.d/fixes/6272-6272-mimo-proxy.md @@ -0,0 +1 @@ +- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) diff --git a/changelog.d/fixes/6276-toolcall-args-logs.md b/changelog.d/fixes/6276-toolcall-args-logs.md new file mode 100644 index 0000000000..90b6bde61b --- /dev/null +++ b/changelog.d/fixes/6276-toolcall-args-logs.md @@ -0,0 +1 @@ +- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) diff --git a/changelog.d/fixes/6280-lmarena-arena-modernize.md b/changelog.d/fixes/6280-lmarena-arena-modernize.md new file mode 100644 index 0000000000..4ba53979bc --- /dev/null +++ b/changelog.d/fixes/6280-lmarena-arena-modernize.md @@ -0,0 +1 @@ +- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun diff --git a/changelog.d/fixes/6308-web-model-discovery.md b/changelog.d/fixes/6308-web-model-discovery.md new file mode 100644 index 0000000000..a452c1ae73 --- /dev/null +++ b/changelog.d/fixes/6308-web-model-discovery.md @@ -0,0 +1 @@ +- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). diff --git a/changelog.d/fixes/6323-log-detail-stale-reopen.md b/changelog.d/fixes/6323-log-detail-stale-reopen.md new file mode 100644 index 0000000000..ec1c53ca2f --- /dev/null +++ b/changelog.d/fixes/6323-log-detail-stale-reopen.md @@ -0,0 +1 @@ +- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). diff --git a/changelog.d/fixes/6330-sensenova-token-plan.md b/changelog.d/fixes/6330-sensenova-token-plan.md new file mode 100644 index 0000000000..3ef26dea2b --- /dev/null +++ b/changelog.d/fixes/6330-sensenova-token-plan.md @@ -0,0 +1 @@ +- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). diff --git a/changelog.d/fixes/6343-6343-v0web-creds.md b/changelog.d/fixes/6343-6343-v0web-creds.md new file mode 100644 index 0000000000..e939d0df24 --- /dev/null +++ b/changelog.d/fixes/6343-6343-v0web-creds.md @@ -0,0 +1 @@ +- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) diff --git a/changelog.d/fixes/6377-6377-agentrouter-key.md b/changelog.d/fixes/6377-6377-agentrouter-key.md new file mode 100644 index 0000000000..c9bc47e827 --- /dev/null +++ b/changelog.d/fixes/6377-6377-agentrouter-key.md @@ -0,0 +1 @@ +- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) diff --git a/changelog.d/fixes/6401-6401-usage-migrations.md b/changelog.d/fixes/6401-6401-usage-migrations.md new file mode 100644 index 0000000000..3d623f9dc6 --- /dev/null +++ b/changelog.d/fixes/6401-6401-usage-migrations.md @@ -0,0 +1 @@ +- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) diff --git a/changelog.d/fixes/6409-6409-build-ram.md b/changelog.d/fixes/6409-6409-build-ram.md new file mode 100644 index 0000000000..4baeb797a6 --- /dev/null +++ b/changelog.d/fixes/6409-6409-build-ram.md @@ -0,0 +1 @@ +- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) diff --git a/changelog.d/fixes/6479-6479-compression-pipeline.md b/changelog.d/fixes/6479-6479-compression-pipeline.md new file mode 100644 index 0000000000..dba6094066 --- /dev/null +++ b/changelog.d/fixes/6479-6479-compression-pipeline.md @@ -0,0 +1 @@ +- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) diff --git a/changelog.d/fixes/6524-6524-reasoning-buffer-clamp.md b/changelog.d/fixes/6524-6524-reasoning-buffer-clamp.md new file mode 100644 index 0000000000..9f551de90c --- /dev/null +++ b/changelog.d/fixes/6524-6524-reasoning-buffer-clamp.md @@ -0,0 +1 @@ +- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) diff --git a/changelog.d/fixes/6538-tier-flow-svg-public.md b/changelog.d/fixes/6538-tier-flow-svg-public.md new file mode 100644 index 0000000000..e3d3f76cef --- /dev/null +++ b/changelog.d/fixes/6538-tier-flow-svg-public.md @@ -0,0 +1 @@ +- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). diff --git a/changelog.d/fixes/6557-6557-disabled-provider.md b/changelog.d/fixes/6557-6557-disabled-provider.md new file mode 100644 index 0000000000..aa924555d3 --- /dev/null +++ b/changelog.d/fixes/6557-6557-disabled-provider.md @@ -0,0 +1 @@ +- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). diff --git a/changelog.d/fixes/6586-preserve-server-tool-names.md b/changelog.d/fixes/6586-preserve-server-tool-names.md new file mode 100644 index 0000000000..62ee75c234 --- /dev/null +++ b/changelog.d/fixes/6586-preserve-server-tool-names.md @@ -0,0 +1 @@ +- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). diff --git a/changelog.d/fixes/6612-combo-diagnostics-header-bytestring-crash.md b/changelog.d/fixes/6612-combo-diagnostics-header-bytestring-crash.md new file mode 100644 index 0000000000..77f12ced1f --- /dev/null +++ b/changelog.d/fixes/6612-combo-diagnostics-header-bytestring-crash.md @@ -0,0 +1 @@ +- fix(sse): sanitize non-Latin1 characters before embedding combo diagnostics in HTTP headers, preventing a ByteString crash on quality-check failure (#6612) diff --git a/changelog.d/fixes/6623-6623-mimo-502-messages.md b/changelog.d/fixes/6623-6623-mimo-502-messages.md new file mode 100644 index 0000000000..5d476c21ef --- /dev/null +++ b/changelog.d/fixes/6623-6623-mimo-502-messages.md @@ -0,0 +1 @@ +- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) diff --git a/changelog.d/fixes/6628-6628-sqljs-adapter.md b/changelog.d/fixes/6628-6628-sqljs-adapter.md new file mode 100644 index 0000000000..55d6a3ce9a --- /dev/null +++ b/changelog.d/fixes/6628-6628-sqljs-adapter.md @@ -0,0 +1,2 @@ +- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) +- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) diff --git a/changelog.d/fixes/6632-probe-restore-loop-cycle-breaker.md b/changelog.d/fixes/6632-probe-restore-loop-cycle-breaker.md new file mode 100644 index 0000000000..45094d3742 --- /dev/null +++ b/changelog.d/fixes/6632-probe-restore-loop-cycle-breaker.md @@ -0,0 +1 @@ +- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). diff --git a/changelog.d/fixes/6634-6634-testmasking-selfref.md b/changelog.d/fixes/6634-6634-testmasking-selfref.md new file mode 100644 index 0000000000..2982803f5a --- /dev/null +++ b/changelog.d/fixes/6634-6634-testmasking-selfref.md @@ -0,0 +1 @@ +- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) diff --git a/changelog.d/fixes/6637-6637-combo-kimi-fallback.md b/changelog.d/fixes/6637-6637-combo-kimi-fallback.md new file mode 100644 index 0000000000..be7cd2ed0b --- /dev/null +++ b/changelog.d/fixes/6637-6637-combo-kimi-fallback.md @@ -0,0 +1 @@ +- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) diff --git a/changelog.d/fixes/6647-winget-claude-detect.md b/changelog.d/fixes/6647-winget-claude-detect.md new file mode 100644 index 0000000000..c0ffef1324 --- /dev/null +++ b/changelog.d/fixes/6647-winget-claude-detect.md @@ -0,0 +1 @@ +- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). diff --git a/changelog.d/fixes/6675-remove-obsolete-providers.md b/changelog.d/fixes/6675-remove-obsolete-providers.md new file mode 100644 index 0000000000..4c4f4bcf1c --- /dev/null +++ b/changelog.d/fixes/6675-remove-obsolete-providers.md @@ -0,0 +1 @@ +- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). diff --git a/changelog.d/fixes/6698-count-gate-rejected-usage.md b/changelog.d/fixes/6698-count-gate-rejected-usage.md new file mode 100644 index 0000000000..bb1288c407 --- /dev/null +++ b/changelog.d/fixes/6698-count-gate-rejected-usage.md @@ -0,0 +1 @@ +- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). diff --git a/changelog.d/fixes/6699-jules-chat-executor-misroute.md b/changelog.d/fixes/6699-jules-chat-executor-misroute.md new file mode 100644 index 0000000000..58823d5879 --- /dev/null +++ b/changelog.d/fixes/6699-jules-chat-executor-misroute.md @@ -0,0 +1 @@ +- fix(providers): reject chat-completions requests for cloud-agent-only providers like jules instead of silently mis-routing them to OpenAI's endpoint (#6699) diff --git a/changelog.d/fixes/6704-unwrap-bare-function-tool.md b/changelog.d/fixes/6704-unwrap-bare-function-tool.md new file mode 100644 index 0000000000..85b21b7c5b --- /dev/null +++ b/changelog.d/fixes/6704-unwrap-bare-function-tool.md @@ -0,0 +1 @@ +- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) diff --git a/changelog.d/fixes/6706-codex-oauth-bare-email-dedup.md b/changelog.d/fixes/6706-codex-oauth-bare-email-dedup.md new file mode 100644 index 0000000000..4eca2c7828 --- /dev/null +++ b/changelog.d/fixes/6706-codex-oauth-bare-email-dedup.md @@ -0,0 +1 @@ +- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) diff --git a/changelog.d/fixes/6710-codex-200-sse-capacity-error.md b/changelog.d/fixes/6710-codex-200-sse-capacity-error.md new file mode 100644 index 0000000000..e84653d7bd --- /dev/null +++ b/changelog.d/fixes/6710-codex-200-sse-capacity-error.md @@ -0,0 +1 @@ +- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) diff --git a/changelog.d/fixes/6712-volcengine-kimi-max-tokens.md b/changelog.d/fixes/6712-volcengine-kimi-max-tokens.md new file mode 100644 index 0000000000..29586d549c --- /dev/null +++ b/changelog.d/fixes/6712-volcengine-kimi-max-tokens.md @@ -0,0 +1 @@ +- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) diff --git a/changelog.d/fixes/6713-antigravity-abort-finish-reason.md b/changelog.d/fixes/6713-antigravity-abort-finish-reason.md new file mode 100644 index 0000000000..f94984faf6 --- /dev/null +++ b/changelog.d/fixes/6713-antigravity-abort-finish-reason.md @@ -0,0 +1 @@ +- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) diff --git a/changelog.d/fixes/6714-reasoning-buffer-cap-aware.md b/changelog.d/fixes/6714-reasoning-buffer-cap-aware.md new file mode 100644 index 0000000000..a67b36333c --- /dev/null +++ b/changelog.d/fixes/6714-reasoning-buffer-cap-aware.md @@ -0,0 +1 @@ +- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev diff --git a/changelog.d/fixes/6717-cli-health-monitoring-route.md b/changelog.d/fixes/6717-cli-health-monitoring-route.md new file mode 100644 index 0000000000..80e5ad7ec7 --- /dev/null +++ b/changelog.d/fixes/6717-cli-health-monitoring-route.md @@ -0,0 +1 @@ +- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. diff --git a/changelog.d/fixes/6718-reasoningcontrols-case-collision.md b/changelog.d/fixes/6718-reasoningcontrols-case-collision.md new file mode 100644 index 0000000000..4c4449755f --- /dev/null +++ b/changelog.d/fixes/6718-reasoningcontrols-case-collision.md @@ -0,0 +1 @@ +- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) diff --git a/changelog.d/fixes/6720-turbopack-agentskills-warning.md b/changelog.d/fixes/6720-turbopack-agentskills-warning.md new file mode 100644 index 0000000000..aa9fc2e42b --- /dev/null +++ b/changelog.d/fixes/6720-turbopack-agentskills-warning.md @@ -0,0 +1 @@ +- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). diff --git a/changelog.d/fixes/6721-codex-spark-image-drop.md b/changelog.d/fixes/6721-codex-spark-image-drop.md new file mode 100644 index 0000000000..d814d8bd39 --- /dev/null +++ b/changelog.d/fixes/6721-codex-spark-image-drop.md @@ -0,0 +1 @@ +- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). diff --git a/changelog.d/fixes/6722-quota-card-fixed-order.md b/changelog.d/fixes/6722-quota-card-fixed-order.md new file mode 100644 index 0000000000..ea2d2f7bbd --- /dev/null +++ b/changelog.d/fixes/6722-quota-card-fixed-order.md @@ -0,0 +1 @@ +- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. diff --git a/changelog.d/fixes/6723-i18n-pt-br-backfill.md b/changelog.d/fixes/6723-i18n-pt-br-backfill.md new file mode 100644 index 0000000000..aca0c58e24 --- /dev/null +++ b/changelog.d/fixes/6723-i18n-pt-br-backfill.md @@ -0,0 +1 @@ +- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). diff --git a/changelog.d/fixes/6725-lazy-ioredis-mcp.md b/changelog.d/fixes/6725-lazy-ioredis-mcp.md new file mode 100644 index 0000000000..ba6238f5ec --- /dev/null +++ b/changelog.d/fixes/6725-lazy-ioredis-mcp.md @@ -0,0 +1 @@ +- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). diff --git a/changelog.d/fixes/6726-kiro-adaptive-thinking-allowlist.md b/changelog.d/fixes/6726-kiro-adaptive-thinking-allowlist.md new file mode 100644 index 0000000000..e31c103661 --- /dev/null +++ b/changelog.d/fixes/6726-kiro-adaptive-thinking-allowlist.md @@ -0,0 +1 @@ +- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. diff --git a/changelog.d/fixes/6729-cursor-subagent-strip-cloud-base.md b/changelog.d/fixes/6729-cursor-subagent-strip-cloud-base.md new file mode 100644 index 0000000000..4cd79fd8a3 --- /dev/null +++ b/changelog.d/fixes/6729-cursor-subagent-strip-cloud-base.md @@ -0,0 +1 @@ +- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) diff --git a/changelog.d/fixes/6730-glm-split-tool-name.md b/changelog.d/fixes/6730-glm-split-tool-name.md new file mode 100644 index 0000000000..ad807f6272 --- /dev/null +++ b/changelog.d/fixes/6730-glm-split-tool-name.md @@ -0,0 +1 @@ +- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) diff --git a/changelog.d/fixes/6731-apikey-429-quota-exhausted.md b/changelog.d/fixes/6731-apikey-429-quota-exhausted.md new file mode 100644 index 0000000000..bdfabc45ef --- /dev/null +++ b/changelog.d/fixes/6731-apikey-429-quota-exhausted.md @@ -0,0 +1 @@ +- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. diff --git a/changelog.d/fixes/6732-fp-pinned-combo-resolve.md b/changelog.d/fixes/6732-fp-pinned-combo-resolve.md new file mode 100644 index 0000000000..47f790681c --- /dev/null +++ b/changelog.d/fixes/6732-fp-pinned-combo-resolve.md @@ -0,0 +1 @@ +- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. diff --git a/changelog.d/fixes/6735-responses-commentary-event-frame.md b/changelog.d/fixes/6735-responses-commentary-event-frame.md new file mode 100644 index 0000000000..6dce5931c9 --- /dev/null +++ b/changelog.d/fixes/6735-responses-commentary-event-frame.md @@ -0,0 +1 @@ +- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). diff --git a/changelog.d/fixes/6741-compression-preview-token-reconcile.md b/changelog.d/fixes/6741-compression-preview-token-reconcile.md new file mode 100644 index 0000000000..122a354265 --- /dev/null +++ b/changelog.d/fixes/6741-compression-preview-token-reconcile.md @@ -0,0 +1 @@ +- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. diff --git a/changelog.d/fixes/6742-quota-preflight-coverage.md b/changelog.d/fixes/6742-quota-preflight-coverage.md new file mode 100644 index 0000000000..45178354e4 --- /dev/null +++ b/changelog.d/fixes/6742-quota-preflight-coverage.md @@ -0,0 +1 @@ +- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a *local cache* already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). diff --git a/changelog.d/fixes/6743-reasoning-content-web-sse.md b/changelog.d/fixes/6743-reasoning-content-web-sse.md new file mode 100644 index 0000000000..089a53d995 --- /dev/null +++ b/changelog.d/fixes/6743-reasoning-content-web-sse.md @@ -0,0 +1 @@ +- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). diff --git a/changelog.d/fixes/6757-rtk-enable-renderers-schema.md b/changelog.d/fixes/6757-rtk-enable-renderers-schema.md new file mode 100644 index 0000000000..a6aa1c910e --- /dev/null +++ b/changelog.d/fixes/6757-rtk-enable-renderers-schema.md @@ -0,0 +1 @@ +- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). diff --git a/changelog.d/fixes/6759-cookie-provider-apikey-cap.md b/changelog.d/fixes/6759-cookie-provider-apikey-cap.md new file mode 100644 index 0000000000..56fa315e97 --- /dev/null +++ b/changelog.d/fixes/6759-cookie-provider-apikey-cap.md @@ -0,0 +1 @@ +- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). diff --git a/changelog.d/fixes/6766-6766-auto-update.md b/changelog.d/fixes/6766-6766-auto-update.md new file mode 100644 index 0000000000..ec08f4a65b --- /dev/null +++ b/changelog.d/fixes/6766-6766-auto-update.md @@ -0,0 +1 @@ +- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) diff --git a/changelog.d/fixes/6769-i18n-translate-pt-dashboard.md b/changelog.d/fixes/6769-i18n-translate-pt-dashboard.md new file mode 100644 index 0000000000..590224dc79 --- /dev/null +++ b/changelog.d/fixes/6769-i18n-translate-pt-dashboard.md @@ -0,0 +1 @@ +- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). diff --git a/changelog.d/fixes/6772-6772-connid-model-400.md b/changelog.d/fixes/6772-6772-connid-model-400.md new file mode 100644 index 0000000000..795c840317 --- /dev/null +++ b/changelog.d/fixes/6772-6772-connid-model-400.md @@ -0,0 +1 @@ +- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) diff --git a/changelog.d/fixes/6773-6773-nim-404.md b/changelog.d/fixes/6773-6773-nim-404.md new file mode 100644 index 0000000000..ba6d5e1543 --- /dev/null +++ b/changelog.d/fixes/6773-6773-nim-404.md @@ -0,0 +1 @@ +- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) diff --git a/changelog.d/fixes/6780-bump-codex-client-version.md b/changelog.d/fixes/6780-bump-codex-client-version.md new file mode 100644 index 0000000000..634ded640e --- /dev/null +++ b/changelog.d/fixes/6780-bump-codex-client-version.md @@ -0,0 +1 @@ +- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo diff --git a/changelog.d/fixes/6788-tia-dashboard-loader.md b/changelog.d/fixes/6788-tia-dashboard-loader.md new file mode 100644 index 0000000000..50e6f4f06b --- /dev/null +++ b/changelog.d/fixes/6788-tia-dashboard-loader.md @@ -0,0 +1 @@ +- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). diff --git a/changelog.d/fixes/6790-gemini-pdf-video-attachments.md b/changelog.d/fixes/6790-gemini-pdf-video-attachments.md new file mode 100644 index 0000000000..ad4cf383d9 --- /dev/null +++ b/changelog.d/fixes/6790-gemini-pdf-video-attachments.md @@ -0,0 +1 @@ +- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). diff --git a/changelog.d/fixes/6791-deepseek-web-done-after-finished.md b/changelog.d/fixes/6791-deepseek-web-done-after-finished.md new file mode 100644 index 0000000000..f7b161c4fd --- /dev/null +++ b/changelog.d/fixes/6791-deepseek-web-done-after-finished.md @@ -0,0 +1 @@ +- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). diff --git a/changelog.d/fixes/6792-compression-put-all-catalog-engines.md b/changelog.d/fixes/6792-compression-put-all-catalog-engines.md new file mode 100644 index 0000000000..813620e098 --- /dev/null +++ b/changelog.d/fixes/6792-compression-put-all-catalog-engines.md @@ -0,0 +1 @@ +- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). diff --git a/changelog.d/fixes/6794-electron-turbopack-symlinks.md b/changelog.d/fixes/6794-electron-turbopack-symlinks.md new file mode 100644 index 0000000000..963e35e6bc --- /dev/null +++ b/changelog.d/fixes/6794-electron-turbopack-symlinks.md @@ -0,0 +1 @@ +- **fix(electron): materialize Turbopack hashed-module symlinks during packaging (#6724, #6594)** (#6794 — thanks @huohua-dev). diff --git a/changelog.d/fixes/6795-cursor-client-version-build-id.md b/changelog.d/fixes/6795-cursor-client-version-build-id.md new file mode 100644 index 0000000000..b82f2fbba2 --- /dev/null +++ b/changelog.d/fixes/6795-cursor-client-version-build-id.md @@ -0,0 +1 @@ +- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). diff --git a/changelog.d/fixes/6800-6800-listen-banner-early.md b/changelog.d/fixes/6800-6800-listen-banner-early.md new file mode 100644 index 0000000000..c53bae5d7f --- /dev/null +++ b/changelog.d/fixes/6800-6800-listen-banner-early.md @@ -0,0 +1 @@ +- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) diff --git a/changelog.d/fixes/6803-6803-flaky-timing-tests.md b/changelog.d/fixes/6803-6803-flaky-timing-tests.md new file mode 100644 index 0000000000..682b5b1887 --- /dev/null +++ b/changelog.d/fixes/6803-6803-flaky-timing-tests.md @@ -0,0 +1 @@ +- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) diff --git a/changelog.d/fixes/6805-strip-include-compact-responses.md b/changelog.d/fixes/6805-strip-include-compact-responses.md new file mode 100644 index 0000000000..6c46a717a4 --- /dev/null +++ b/changelog.d/fixes/6805-strip-include-compact-responses.md @@ -0,0 +1 @@ +- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). diff --git a/changelog.d/fixes/6806-6806-claude-quota-data.md b/changelog.d/fixes/6806-6806-claude-quota-data.md new file mode 100644 index 0000000000..bbc9f76803 --- /dev/null +++ b/changelog.d/fixes/6806-6806-claude-quota-data.md @@ -0,0 +1 @@ +- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) diff --git a/changelog.d/fixes/6812-request-count-by-provider-date.md b/changelog.d/fixes/6812-request-count-by-provider-date.md new file mode 100644 index 0000000000..503897954e --- /dev/null +++ b/changelog.d/fixes/6812-request-count-by-provider-date.md @@ -0,0 +1 @@ +- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) diff --git a/changelog.d/fixes/6817-ollama-cloud-weekly-quota.md b/changelog.d/fixes/6817-ollama-cloud-weekly-quota.md new file mode 100644 index 0000000000..9ff2857cf5 --- /dev/null +++ b/changelog.d/fixes/6817-ollama-cloud-weekly-quota.md @@ -0,0 +1 @@ +- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. diff --git a/changelog.d/fixes/6821-budget-tokens-zero-gemini.md b/changelog.d/fixes/6821-budget-tokens-zero-gemini.md new file mode 100644 index 0000000000..3886b4e6b3 --- /dev/null +++ b/changelog.d/fixes/6821-budget-tokens-zero-gemini.md @@ -0,0 +1 @@ +- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). diff --git a/changelog.d/fixes/6828-bootstrap-filter-empty-env.md b/changelog.d/fixes/6828-bootstrap-filter-empty-env.md new file mode 100644 index 0000000000..d4a6ca34ad --- /dev/null +++ b/changelog.d/fixes/6828-bootstrap-filter-empty-env.md @@ -0,0 +1 @@ +- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). diff --git a/changelog.d/fixes/6829-classify-404-model-not-found.md b/changelog.d/fixes/6829-classify-404-model-not-found.md new file mode 100644 index 0000000000..77a741377c --- /dev/null +++ b/changelog.d/fixes/6829-classify-404-model-not-found.md @@ -0,0 +1 @@ +- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). diff --git a/changelog.d/fixes/6835-db-oom-probe-cyclebreaker.md b/changelog.d/fixes/6835-db-oom-probe-cyclebreaker.md new file mode 100644 index 0000000000..3208171d3f --- /dev/null +++ b/changelog.d/fixes/6835-db-oom-probe-cyclebreaker.md @@ -0,0 +1 @@ +- **fix(db):** cap the sql.js OOM-during-probe path in `getDbInstance()` at 3 attempts with a terminal diagnostic — previously only the generic-corruption probe-failure path had a cycle-breaker (#6632), so a persistently OOMing `storage.sqlite` probe re-threw the identical error forever on every call from every background poller, hanging the app with "Internal Server Error" and no self-recovery (#6835). diff --git a/changelog.d/fixes/6854-6854-mcp-toolcount.md b/changelog.d/fixes/6854-6854-mcp-toolcount.md new file mode 100644 index 0000000000..1e0ac6b1fe --- /dev/null +++ b/changelog.d/fixes/6854-6854-mcp-toolcount.md @@ -0,0 +1 @@ +- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) diff --git a/changelog.d/fixes/6856-usage-exact-cost-strict-validation.md b/changelog.d/fixes/6856-usage-exact-cost-strict-validation.md new file mode 100644 index 0000000000..abc5c3bc84 --- /dev/null +++ b/changelog.d/fixes/6856-usage-exact-cost-strict-validation.md @@ -0,0 +1 @@ +- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) diff --git a/changelog.d/fixes/6859-plugin-provider-id.md b/changelog.d/fixes/6859-plugin-provider-id.md new file mode 100644 index 0000000000..9343798009 --- /dev/null +++ b/changelog.d/fixes/6859-plugin-provider-id.md @@ -0,0 +1 @@ +- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). diff --git a/changelog.d/fixes/6876-6876-cliproxy-modelmap.md b/changelog.d/fixes/6876-6876-cliproxy-modelmap.md new file mode 100644 index 0000000000..d28d68467b --- /dev/null +++ b/changelog.d/fixes/6876-6876-cliproxy-modelmap.md @@ -0,0 +1 @@ +- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) diff --git a/changelog.d/fixes/6906-responses-usage-trailing.md b/changelog.d/fixes/6906-responses-usage-trailing.md new file mode 100644 index 0000000000..bfa9691b4b --- /dev/null +++ b/changelog.d/fixes/6906-responses-usage-trailing.md @@ -0,0 +1 @@ +- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) diff --git a/changelog.d/fixes/6908-standalone-head-response-guard.md b/changelog.d/fixes/6908-standalone-head-response-guard.md new file mode 100644 index 0000000000..3bb36703a6 --- /dev/null +++ b/changelog.d/fixes/6908-standalone-head-response-guard.md @@ -0,0 +1 @@ +- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) diff --git a/changelog.d/fixes/6911-quota-throttle-all-fetchers.md b/changelog.d/fixes/6911-quota-throttle-all-fetchers.md new file mode 100644 index 0000000000..0983d10b27 --- /dev/null +++ b/changelog.d/fixes/6911-quota-throttle-all-fetchers.md @@ -0,0 +1 @@ +- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) diff --git a/changelog.d/fixes/6912-volcengine-max-tokens-rename.md b/changelog.d/fixes/6912-volcengine-max-tokens-rename.md new file mode 100644 index 0000000000..88d6ac0e3d --- /dev/null +++ b/changelog.d/fixes/6912-volcengine-max-tokens-rename.md @@ -0,0 +1 @@ +- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) diff --git a/changelog.d/fixes/6914-antigravity-server-side-tool-invocations.md b/changelog.d/fixes/6914-antigravity-server-side-tool-invocations.md new file mode 100644 index 0000000000..858201625b --- /dev/null +++ b/changelog.d/fixes/6914-antigravity-server-side-tool-invocations.md @@ -0,0 +1 @@ +- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) diff --git a/changelog.d/fixes/6925-embeddings-lan-noauth.md b/changelog.d/fixes/6925-embeddings-lan-noauth.md new file mode 100644 index 0000000000..03bd51b6d1 --- /dev/null +++ b/changelog.d/fixes/6925-embeddings-lan-noauth.md @@ -0,0 +1 @@ +- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) diff --git a/changelog.d/fixes/6927-qwen-web-content-array.md b/changelog.d/fixes/6927-qwen-web-content-array.md new file mode 100644 index 0000000000..5eff8b6ed9 --- /dev/null +++ b/changelog.d/fixes/6927-qwen-web-content-array.md @@ -0,0 +1 @@ +- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) diff --git a/changelog.d/fixes/6930-relay-checkratelimit-token-reuse.md b/changelog.d/fixes/6930-relay-checkratelimit-token-reuse.md new file mode 100644 index 0000000000..e52bb9d46a --- /dev/null +++ b/changelog.d/fixes/6930-relay-checkratelimit-token-reuse.md @@ -0,0 +1 @@ +- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) diff --git a/changelog.d/fixes/6932-codex-assistant-input-text-normalization.md b/changelog.d/fixes/6932-codex-assistant-input-text-normalization.md new file mode 100644 index 0000000000..3ae7dc8dc9 --- /dev/null +++ b/changelog.d/fixes/6932-codex-assistant-input-text-normalization.md @@ -0,0 +1 @@ +- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) diff --git a/changelog.d/fixes/6935-muse-spark-attachments.md b/changelog.d/fixes/6935-muse-spark-attachments.md new file mode 100644 index 0000000000..e2a8f4d3ed --- /dev/null +++ b/changelog.d/fixes/6935-muse-spark-attachments.md @@ -0,0 +1 @@ +- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) diff --git a/changelog.d/fixes/6936-providercard-audio-badge.md b/changelog.d/fixes/6936-providercard-audio-badge.md new file mode 100644 index 0000000000..6b70fb4434 --- /dev/null +++ b/changelog.d/fixes/6936-providercard-audio-badge.md @@ -0,0 +1 @@ +- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) diff --git a/changelog.d/fixes/6939-lmstudio-models-guard.md b/changelog.d/fixes/6939-lmstudio-models-guard.md new file mode 100644 index 0000000000..538a2e5769 --- /dev/null +++ b/changelog.d/fixes/6939-lmstudio-models-guard.md @@ -0,0 +1 @@ +- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) diff --git a/changelog.d/fixes/6943-gemini-thinking-none-effort.md b/changelog.d/fixes/6943-gemini-thinking-none-effort.md new file mode 100644 index 0000000000..0c6f67f2b9 --- /dev/null +++ b/changelog.d/fixes/6943-gemini-thinking-none-effort.md @@ -0,0 +1 @@ +- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) diff --git a/changelog.d/fixes/6944-chatgpt-web-citation-backslash-escape.md b/changelog.d/fixes/6944-chatgpt-web-citation-backslash-escape.md new file mode 100644 index 0000000000..cefb3d8800 --- /dev/null +++ b/changelog.d/fixes/6944-chatgpt-web-citation-backslash-escape.md @@ -0,0 +1 @@ +- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl diff --git a/changelog.d/fixes/6947-tokenhealthcheck-provider-case.md b/changelog.d/fixes/6947-tokenhealthcheck-provider-case.md new file mode 100644 index 0000000000..d9b19fa4e9 --- /dev/null +++ b/changelog.d/fixes/6947-tokenhealthcheck-provider-case.md @@ -0,0 +1 @@ +- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) diff --git a/changelog.d/fixes/6951-codex-optional-tool-params.md b/changelog.d/fixes/6951-codex-optional-tool-params.md new file mode 100644 index 0000000000..2d634fd24c --- /dev/null +++ b/changelog.d/fixes/6951-codex-optional-tool-params.md @@ -0,0 +1 @@ +- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) diff --git a/changelog.d/fixes/6952-commentary-translate-mode.md b/changelog.d/fixes/6952-commentary-translate-mode.md new file mode 100644 index 0000000000..07dd100b32 --- /dev/null +++ b/changelog.d/fixes/6952-commentary-translate-mode.md @@ -0,0 +1 @@ +- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) diff --git a/changelog.d/fixes/6975-6957-combo-builder-models.md b/changelog.d/fixes/6975-6957-combo-builder-models.md new file mode 100644 index 0000000000..46f0154967 --- /dev/null +++ b/changelog.d/fixes/6975-6957-combo-builder-models.md @@ -0,0 +1,2 @@ +- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). +- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). diff --git a/changelog.d/fixes/6996-duckduckgo-vqd-429-misclassified.md b/changelog.d/fixes/6996-duckduckgo-vqd-429-misclassified.md new file mode 100644 index 0000000000..36b15a9cbd --- /dev/null +++ b/changelog.d/fixes/6996-duckduckgo-vqd-429-misclassified.md @@ -0,0 +1 @@ +- fix(providers): DuckDuckGo AI Chat executor propagates the real upstream status (429 rate limit with `Retry-After`) instead of misclassifying VQD-token acquisition failures as a hardcoded 503 (#6996) diff --git a/changelog.d/fixes/6998-opencode-oc-free-tier-catalog.md b/changelog.d/fixes/6998-opencode-oc-free-tier-catalog.md new file mode 100644 index 0000000000..e5a3f5189b --- /dev/null +++ b/changelog.d/fixes/6998-opencode-oc-free-tier-catalog.md @@ -0,0 +1 @@ +- fix(providers): refresh OpenCode (`oc`) free-tier model catalog — 6 delisted IDs replaced with the 4 currently-live free models (#6998) diff --git a/changelog.d/fixes/7003-jetbrains-ai-loopback-connect.md b/changelog.d/fixes/7003-jetbrains-ai-loopback-connect.md new file mode 100644 index 0000000000..bc3b0a89c4 --- /dev/null +++ b/changelog.d/fixes/7003-jetbrains-ai-loopback-connect.md @@ -0,0 +1 @@ +- fix(api): raise the main server's `keepAliveTimeout`/`headersTimeout` well above Node's 5s default so pooled keep-alive clients (e.g. JetBrains AI Assistant's JVM `HttpClient`) stop getting 0 bytes back on a reused connection (#7003) diff --git a/changelog.d/fixes/7005-adaptive-context-budget-dial.md b/changelog.d/fixes/7005-adaptive-context-budget-dial.md new file mode 100644 index 0000000000..4b8e8aee5b --- /dev/null +++ b/changelog.d/fixes/7005-adaptive-context-budget-dial.md @@ -0,0 +1 @@ +- fix(compression): wire the adaptive context-budget "dial" (`contextBudget`) into the settings schema and DB so it can actually be persisted via `PUT /api/settings/compression`, instead of being silently rejected (#7005) diff --git a/changelog.d/fixes/7022-opencode-go-quota-url-zai.md b/changelog.d/fixes/7022-opencode-go-quota-url-zai.md new file mode 100644 index 0000000000..e136c66551 --- /dev/null +++ b/changelog.d/fixes/7022-opencode-go-quota-url-zai.md @@ -0,0 +1 @@ +- fix(usage): stop opencode-go quota lookup from defaulting to an unrelated Z.AI endpoint (#7022) diff --git a/changelog.d/fixes/7033-dashboard-typecheck-gate.md b/changelog.d/fixes/7033-dashboard-typecheck-gate.md new file mode 100644 index 0000000000..89a33f44d6 --- /dev/null +++ b/changelog.d/fixes/7033-dashboard-typecheck-gate.md @@ -0,0 +1 @@ +- fix(ci): add a dashboard-scoped typecheck gate covering `src/app/(dashboard)` TSX, previously invisible to `typecheck:core` and `next build` (#7033) diff --git a/changelog.d/fixes/7051-turbopack-compression-ignoreissue.md b/changelog.d/fixes/7051-turbopack-compression-ignoreissue.md new file mode 100644 index 0000000000..017b5b8c78 --- /dev/null +++ b/changelog.d/fixes/7051-turbopack-compression-ignoreissue.md @@ -0,0 +1 @@ +- fix(build): extend the Turbopack `ignoreIssue` suppression to `open-sse/services/compression/**`, matching the `getModuleDir()` dynamic-path fs pattern already suppressed for `src/lib/agentSkills/**` in #6582, eliminating the remaining 610 "Overly broad patterns" warnings (#7051) diff --git a/changelog.d/fixes/7058-zai-web-proxy-connection-test.md b/changelog.d/fixes/7058-zai-web-proxy-connection-test.md new file mode 100644 index 0000000000..f7d453d734 --- /dev/null +++ b/changelog.d/fixes/7058-zai-web-proxy-connection-test.md @@ -0,0 +1 @@ +- fix(providers): web-cookie connection-test/cookie-validation probe (zai-web and every other registry-entry web-cookie provider) now honors the configured HTTP/SOCKS proxy — the `/models` probe routed through `directHttpsRequest`'s hardcoded native-fetch bypass, silently skipping proxy resolution even though the executor's actual chat traffic already respected it (#7058) diff --git a/changelog.d/fixes/7071-ollama-cloud-session-quota.md b/changelog.d/fixes/7071-ollama-cloud-session-quota.md new file mode 100644 index 0000000000..81807e30ad --- /dev/null +++ b/changelog.d/fixes/7071-ollama-cloud-session-quota.md @@ -0,0 +1 @@ +- fix(resilience): recognize Ollama Cloud's 5-hour session usage-limit 429 as quota-exhausted instead of a generic rate limit (#7071) diff --git a/changelog.d/fixes/7072-quota-card-grid-mobile.md b/changelog.d/fixes/7072-quota-card-grid-mobile.md new file mode 100644 index 0000000000..df74be8a75 --- /dev/null +++ b/changelog.d/fixes/7072-quota-card-grid-mobile.md @@ -0,0 +1 @@ +- fix(dashboard): restore mobile single-column fallback on the Provider Quota page card grid, fixing clipped labels/buttons on phone-width viewports (#7072) diff --git a/changelog.d/fixes/7080-proxy-test-connection-saved-proxyid.md b/changelog.d/fixes/7080-proxy-test-connection-saved-proxyid.md new file mode 100644 index 0000000000..cc374b6a8a --- /dev/null +++ b/changelog.d/fixes/7080-proxy-test-connection-saved-proxyid.md @@ -0,0 +1 @@ +- fix(dashboard): include proxyId when testing a saved registry proxy so SOCKS5/auth credentials are loaded (#7080) diff --git a/changelog.d/fixes/7098-mimo-thinking-model-reasoning-content.md b/changelog.d/fixes/7098-mimo-thinking-model-reasoning-content.md new file mode 100644 index 0000000000..ddd454f252 --- /dev/null +++ b/changelog.d/fixes/7098-mimo-thinking-model-reasoning-content.md @@ -0,0 +1 @@ +- **fix(sse):** xiaomi-tokenplan `mimo` models (e.g. `mimo-v2.5-pro`) are now recognized as thinking-mode upstreams that require `reasoning_content` echoed back on every assistant turn, fixing a persistent `400 reasoning_content must be passed back` error on multi-turn conversations ([#7098](https://github.com/diegosouzapw/OmniRoute/pull/7098)) — thanks @xxue-z diff --git a/changelog.d/fixes/7125-onboarding-tiers-layout.md b/changelog.d/fixes/7125-onboarding-tiers-layout.md new file mode 100644 index 0000000000..8bb9f718eb --- /dev/null +++ b/changelog.d/fixes/7125-onboarding-tiers-layout.md @@ -0,0 +1 @@ +- **fix(dashboard):** align onboarding tier descriptions and localize the tier step header and flow copy ([#7125](https://github.com/diegosouzapw/OmniRoute/pull/7125)) — thanks @Wibias diff --git a/changelog.d/fixes/7134-claude-web-400-no-response-body.md b/changelog.d/fixes/7134-claude-web-400-no-response-body.md new file mode 100644 index 0000000000..f9fd94cf5f --- /dev/null +++ b/changelog.d/fixes/7134-claude-web-400-no-response-body.md @@ -0,0 +1 @@ +- **fix(sse):** claude-web now surfaces the real upstream error body for non-SSE 400/403/429/500 responses instead of reporting "no response body" — the streaming client was discarding the already-captured temp-file bytes and reading the native binding's empty in-memory body field instead (#7134). diff --git a/changelog.d/fixes/7149-combo-scope-proxy-dead.md b/changelog.d/fixes/7149-combo-scope-proxy-dead.md new file mode 100644 index 0000000000..fcbbccb065 --- /dev/null +++ b/changelog.d/fixes/7149-combo-scope-proxy-dead.md @@ -0,0 +1 @@ +- fix(db): honor combo-level proxy assignments from the registry when resolving a connection's proxy (#7149) diff --git a/changelog.d/fixes/7151-hermes-agent-model-aliases.md b/changelog.d/fixes/7151-hermes-agent-model-aliases.md new file mode 100644 index 0000000000..b408b03462 --- /dev/null +++ b/changelog.d/fixes/7151-hermes-agent-model-aliases.md @@ -0,0 +1 @@ +- fix(dashboard): wire modelAliases fetch into HermesAgentToolCard so OpenRouter and other passthrough providers appear in the Hermes Agent role picker (#7151) diff --git a/changelog.d/fixes/7156-legacy-model-picker-hidden-filter.md b/changelog.d/fixes/7156-legacy-model-picker-hidden-filter.md new file mode 100644 index 0000000000..cb3edb1642 --- /dev/null +++ b/changelog.d/fixes/7156-legacy-model-picker-hidden-filter.md @@ -0,0 +1 @@ +- fix(dashboard): filter hidden custom models out of the legacy combo model picker (#7156) diff --git a/changelog.d/fixes/7157-agent-bridge-dns-405.md b/changelog.d/fixes/7157-agent-bridge-dns-405.md new file mode 100644 index 0000000000..98a887ce2a --- /dev/null +++ b/changelog.d/fixes/7157-agent-bridge-dns-405.md @@ -0,0 +1 @@ +- fix(dashboard): Agent Bridge DNS toggle now sends POST (was PUT), fixing HTTP 405 on Start/Stop DNS (#7157) diff --git a/changelog.d/fixes/7161-free-pool-handletogglesource.md b/changelog.d/fixes/7161-free-pool-handletogglesource.md new file mode 100644 index 0000000000..b85d16d651 --- /dev/null +++ b/changelog.d/fixes/7161-free-pool-handletogglesource.md @@ -0,0 +1 @@ +- fix(dashboard): implement missing `handleToggleSource` callback on the Free Pool tab so the page no longer crashes with a `ReferenceError` (#7161) diff --git a/changelog.d/fixes/7163-gemini-web-duplicated-text.md b/changelog.d/fixes/7163-gemini-web-duplicated-text.md new file mode 100644 index 0000000000..6bac17d7e8 --- /dev/null +++ b/changelog.d/fixes/7163-gemini-web-duplicated-text.md @@ -0,0 +1 @@ +- fix(sse): stop duplicating text in Gemini Web streamed responses (#7163) diff --git a/changelog.d/fixes/pack-boot-runtimetimeouts-sibling.md b/changelog.d/fixes/pack-boot-runtimetimeouts-sibling.md new file mode 100644 index 0000000000..3fd32f4f02 --- /dev/null +++ b/changelog.d/fixes/pack-boot-runtimetimeouts-sibling.md @@ -0,0 +1 @@ +- **Build**: the packed tarball boots again — #7191's `../../src/…runtimeTimeouts.ts` import in `standalone-server-ws.mjs` escaped the package after the dist-root copy (`ERR_MODULE_NOT_FOUND` on every boot, #7065 class, caught live by the new `check:pack-boot` gate); the helper now lives in the shipped sibling `main-server-timeouts.mjs` (parity-tested against the canonical TS implementation) and the closure test bans package-escaping `../` imports in npm-shipped wrappers diff --git a/changelog.d/fixes/pack-boot-smoke-gate.md b/changelog.d/fixes/pack-boot-smoke-gate.md new file mode 100644 index 0000000000..cc44649218 --- /dev/null +++ b/changelog.d/fixes/pack-boot-smoke-gate.md @@ -0,0 +1 @@ +- **Packaging**: new `check:pack-boot` gate packs the real npm tarball, installs it into a clean prefix (postinstall runs for real) and boots the installed CLI until `/api/monitoring/health` returns 200 with the packed version — the runtime gate that structure checks could not provide (3 releases shipped boot-crashing tarballs with green packaging lists: tls-options/3.8.41, #7040, #7065). Wired into the CI `package-artifact` job and `check:release-green --with-build` diff --git a/changelog.d/fixes/pack-entrypoint-closures.md b/changelog.d/fixes/pack-entrypoint-closures.md new file mode 100644 index 0000000000..09e114614d --- /dev/null +++ b/changelog.d/fixes/pack-entrypoint-closures.md @@ -0,0 +1 @@ +- **Packaging**: pack-artifact closure tests now cover EVERY npm-shipped dist wrapper (derived from `EXTRA_MODULE_ENTRIES`) and the `bin/omniroute.mjs` CLI entry — including dynamic `import()` and `require()` forms the original server-ws test missed — requiring each local import in both the prepublish prune allowlist and `check:pack-artifact`; `bin/cli/data-dir.mjs` and `bin/cli/utils/storageKeyProvision.mjs` are now required tarball paths (#7065 class hardening, WS1.1) diff --git a/changelog.d/fixes/port-2132-headroom-developer-role.md b/changelog.d/fixes/port-2132-headroom-developer-role.md new file mode 100644 index 0000000000..c9064e43c5 --- /dev/null +++ b/changelog.d/fixes/port-2132-headroom-developer-role.md @@ -0,0 +1 @@ +- **fix(compression):** the Headroom SmartCrusher tabular-compaction guard now also skips `role: "developer"` messages, not just `role: "system"` — Codex CLI sends its instructions/tool-schema turn as `developer` (the Responses-API equivalent of `system`), so an embedded JSON array (e.g. an `update_plan` example) could get tabular-compacted, corrupting the model's tool-calling instructions and breaking Codex CLI plan mode. (thanks @SingCJ) diff --git a/changelog.d/fixes/register-cli-skill-collector.md b/changelog.d/fixes/register-cli-skill-collector.md new file mode 100644 index 0000000000..f7ba19ba92 --- /dev/null +++ b/changelog.d/fixes/register-cli-skill-collector.md @@ -0,0 +1 @@ +- **Skills**: register `cli-skill-collector` in the agent-skills catalog (types union, curated entry, CLI id list) — #6294 shipped the `skills/cli-skill-collector/` directory without the catalog registration, so it was unreachable via the API and Integration CI failed on the catalog-integrity test; counts aligned (44 API+CLI, 45 with config) diff --git a/changelog.d/fixes/yuanbao-cookie-validation.md b/changelog.d/fixes/yuanbao-cookie-validation.md new file mode 100644 index 0000000000..eb91b6edba --- /dev/null +++ b/changelog.d/fixes/yuanbao-cookie-validation.md @@ -0,0 +1 @@ +- **Providers**: yuanbao-web no longer forwards a foreign single cookie pair upstream — `buildYuanbaoCookie` only trusts `hy_user`/`hy_token` extractions the input explicitly names, so a missing session token now fails fast with the local 401 guidance instead of a live Tencent round-trip diff --git a/changelog.d/maintenance/6784-merge-train.md b/changelog.d/maintenance/6784-merge-train.md new file mode 100644 index 0000000000..e98586183f --- /dev/null +++ b/changelog.d/maintenance/6784-merge-train.md @@ -0,0 +1 @@ +- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. diff --git a/changelog.d/maintenance/6878-list-uncovered-fragments.md b/changelog.d/maintenance/6878-list-uncovered-fragments.md new file mode 100644 index 0000000000..de5fb61642 --- /dev/null +++ b/changelog.d/maintenance/6878-list-uncovered-fragments.md @@ -0,0 +1 @@ +- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) diff --git a/changelog.d/maintenance/7068-dependabot-ignore-typescript-major.md b/changelog.d/maintenance/7068-dependabot-ignore-typescript-major.md new file mode 100644 index 0000000000..ae20696c5e --- /dev/null +++ b/changelog.d/maintenance/7068-dependabot-ignore-typescript-major.md @@ -0,0 +1 @@ +- **chore(ci):** stop dependabot from proposing `typescript` majors — `typescript-eslint` pins a hard peer upper bound (`>=4.8.4 <6.1.0`), so a TS 7 bump violates the peer and takes the whole toolchain red at once. #7068 grouped it with 6 harmless dev bumps and blocked all of them. TS majors now migrate intentionally, in their own PR. diff --git a/changelog.d/maintenance/7291-quota-card-grid-density-6815-guard.md b/changelog.d/maintenance/7291-quota-card-grid-density-6815-guard.md new file mode 100644 index 0000000000..6652969e7d --- /dev/null +++ b/changelog.d/maintenance/7291-quota-card-grid-density-6815-guard.md @@ -0,0 +1 @@ +- **test(dashboard):** restore dedicated regression coverage for #6815's `QuotaCardGrid` multi-column density guarantee, decoupled from the specific Tailwind token so it survives the #7027 auto-fit migration ([#7291](https://github.com/diegosouzapw/OmniRoute/pull/7291)) diff --git a/changelog.d/maintenance/7307-rehome-open-prs-script.md b/changelog.d/maintenance/7307-rehome-open-prs-script.md new file mode 100644 index 0000000000..e20161f4bc --- /dev/null +++ b/changelog.d/maintenance/7307-rehome-open-prs-script.md @@ -0,0 +1 @@ +- **chore(release):** add `scripts/release/rehome-open-prs.mjs` — the Phase 0a.0b PR re-home, scripted with a read-back after every retarget. `gh pr edit --base` exits 0 without applying (v3.8.42), `gh pr list` silently caps at 30, and the v3.8.49 freeze had 148 open PRs to move — none of which a hand-run loop survives reliably. diff --git a/changelog.d/maintenance/basereds-captain-2026-07-12.md b/changelog.d/maintenance/basereds-captain-2026-07-12.md new file mode 100644 index 0000000000..b8d359a762 --- /dev/null +++ b/changelog.d/maintenance/basereds-captain-2026-07-12.md @@ -0,0 +1 @@ +- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) diff --git a/changelog.d/maintenance/basereds-i18n-gemini-2026-07-13.md b/changelog.d/maintenance/basereds-i18n-gemini-2026-07-13.md new file mode 100644 index 0000000000..cdadafc8b6 --- /dev/null +++ b/changelog.d/maintenance/basereds-i18n-gemini-2026-07-13.md @@ -0,0 +1 @@ +- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract diff --git a/changelog.d/maintenance/basereds-mechanical-2026-07-12.md b/changelog.d/maintenance/basereds-mechanical-2026-07-12.md new file mode 100644 index 0000000000..f1ee83c06f --- /dev/null +++ b/changelog.d/maintenance/basereds-mechanical-2026-07-12.md @@ -0,0 +1 @@ +- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) diff --git a/changelog.d/maintenance/ci-pr-test-policy-shallow-base.md b/changelog.d/maintenance/ci-pr-test-policy-shallow-base.md new file mode 100644 index 0000000000..340b60a5ce --- /dev/null +++ b/changelog.d/maintenance/ci-pr-test-policy-shallow-base.md @@ -0,0 +1 @@ +- CI: `pr-test-policy` fetches the base branch with full history instead of `--depth=1` — the shallow graft made `merge-base` resolve wrong for PR branches that recently merged the release, so the three-dot diff blamed the PR for OTHER merged PRs' changes (false "deleted test"/"weakened asserts" reds; observed on #7329 being blamed for #7106's files). diff --git a/changelog.d/maintenance/ci-vps-runner-sharding.md b/changelog.d/maintenance/ci-vps-runner-sharding.md new file mode 100644 index 0000000000..925ce479a4 --- /dev/null +++ b/changelog.d/maintenance/ci-vps-runner-sharding.md @@ -0,0 +1 @@ +- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). diff --git a/changelog.d/maintenance/codecov-patch-coverage.md b/changelog.d/maintenance/codecov-patch-coverage.md new file mode 100644 index 0000000000..6d023220d6 --- /dev/null +++ b/changelog.d/maintenance/codecov-patch-coverage.md @@ -0,0 +1 @@ +- **CI**: Codecov patch-coverage on every PR diff (informational during calibration — `codecov.yml` sets nothing blocking; strict-patch/lenient-project philosophy on top of the existing 60% c8 floor + ratchet); the CI coverage job now actually emits `coverage/lcov.info` (the `lcov` reporter was missing, so the artifact silently skipped it — the same file Sonar consumes) diff --git a/changelog.d/maintenance/coverage-job-timeout.md b/changelog.d/maintenance/coverage-job-timeout.md new file mode 100644 index 0000000000..ce36f15f3f --- /dev/null +++ b/changelog.d/maintenance/coverage-job-timeout.md @@ -0,0 +1 @@ +- **CI**: raise the Coverage job timeout 10→20min — the lcov reporter added for Codecov/Sonar (#7114) pushed the 8-shard report merge past the old cap, and three release-tip runs died at exactly 10min as job-timeout "cancelled" diff --git a/changelog.d/maintenance/dast-smoke-timeout.md b/changelog.d/maintenance/dast-smoke-timeout.md new file mode 100644 index 0000000000..b8c9880570 --- /dev/null +++ b/changelog.d/maintenance/dast-smoke-timeout.md @@ -0,0 +1 @@ +- **CI**: raise the dast-smoke job timeout 12→25min — the CLI bundle build alone varies 6-11min on GitHub-hosted runners, so the old cap killed Schemathesis mid-run (3 consecutive false-negative timeouts on 2026-07-14) diff --git a/changelog.d/maintenance/docs-strategy-count-skill-names.md b/changelog.d/maintenance/docs-strategy-count-skill-names.md new file mode 100644 index 0000000000..a14507159b --- /dev/null +++ b/changelog.d/maintenance/docs-strategy-count-skill-names.md @@ -0,0 +1 @@ +- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). diff --git a/changelog.d/maintenance/e2e-shard-balance.md b/changelog.d/maintenance/e2e-shard-balance.md new file mode 100644 index 0000000000..a950f1ba44 --- /dev/null +++ b/changelog.d/maintenance/e2e-shard-balance.md @@ -0,0 +1 @@ +- **CI**: E2E matrix shards are now duration-balanced (LPT bin-packing over `config/quality/e2e-timings.json`) instead of Playwright's count-based `--shard` — measured skew was 14× (24m47s vs 1m47s), making E2E the CI critical path; the balancer self-verifies that every spec lands in exactly one shard and falls back to plain `--shard` on any inconsistency diff --git a/changelog.d/maintenance/electron-win-advisory.md b/changelog.d/maintenance/electron-win-advisory.md new file mode 100644 index 0000000000..c5f29e8404 --- /dev/null +++ b/changelog.d/maintenance/electron-win-advisory.md @@ -0,0 +1 @@ +- **CI**: the new Electron Windows prepare-bundle leg (WS1.5) is advisory while it matures — its first real run failed with the error swallowed by pwsh; the step now runs under bash (stderr captured) with `continue-on-error`, tracked for promotion once green diff --git a/changelog.d/maintenance/electron-win-prepare-smoke.md b/changelog.d/maintenance/electron-win-prepare-smoke.md new file mode 100644 index 0000000000..2fcdfe3740 --- /dev/null +++ b/changelog.d/maintenance/electron-win-prepare-smoke.md @@ -0,0 +1 @@ +- **CI**: the Electron Package Smoke job now runs a Windows leg that executes `prepare:bundle` (the native ABI rebuild + spawn plan) per release PR — the v3.8.48 Windows bug (`npx.cmd` spawned without shell) could previously only surface on the release tag, its first-ever execution diff --git a/changelog.d/maintenance/gate-hygiene-secrets-semgrep-hadolint.md b/changelog.d/maintenance/gate-hygiene-secrets-semgrep-hadolint.md new file mode 100644 index 0000000000..7009f36ab2 --- /dev/null +++ b/changelog.d/maintenance/gate-hygiene-secrets-semgrep-hadolint.md @@ -0,0 +1 @@ +- **Quality gates hygiene (WS6/D3 + WS1.7)**: gitleaks baseline zeroed — the 3 frozen `generic-api-key` false positives (latency field names + the public Anthropic beta-header value) are allowlisted with justification, so any NEW secret finding now regresses the ratchet from 0; the orphaned `semgrepFindings` baseline metric was dropped (never wired to a gate; CodeQL covers the OWASP families); Dockerfile now has a hadolint gate in the lint job (digest-pinned, error-threshold — the 5 pre-existing warnings stay visible without blocking) diff --git a/changelog.d/maintenance/hotfix-fastlane.md b/changelog.d/maintenance/hotfix-fastlane.md new file mode 100644 index 0000000000..a7110c9a79 --- /dev/null +++ b/changelog.d/maintenance/hotfix-fastlane.md @@ -0,0 +1 @@ +- **CI**: hotfix fast-lane — PRs labeled `hotfix` (owner-applied, production-broken only; entry policy in `docs/ops/RELEASE_CHECKLIST.md`) skip the 9-shard E2E matrix, coverage ratchet and extended gates while keeping build, unit/integration/vitest, lint/typecheck and the tarball boot-smoke (~15min instead of ~33min); tests-only diffs outside `tests/e2e/` skip the E2E matrix automatically via the new `testsOnly` change-classification output diff --git a/changelog.d/maintenance/mergify-queue-draft-ci.md b/changelog.d/maintenance/mergify-queue-draft-ci.md new file mode 100644 index 0000000000..63469d2c33 --- /dev/null +++ b/changelog.d/maintenance/mergify-queue-draft-ci.md @@ -0,0 +1 @@ +- **CI**: quality.yml draft guards now also match Mergify speculative merge-queue PRs (`mergify/merge-queue/*` heads are drafts by design) — without this every queued batch failed its anchor check in 2s and dequeued diff --git a/changelog.d/maintenance/mergify-queue.md b/changelog.d/maintenance/mergify-queue.md new file mode 100644 index 0000000000..8ffe6bb43a --- /dev/null +++ b/changelog.d/maintenance/mergify-queue.md @@ -0,0 +1 @@ +- **Merge queue (D5)**: reviewed PRs now merge through the Mergify queue (`.mergify.yml`, Open Source plan) — entry is the owner-applied `queue` label AFTER the pre-merge ⭐ gate; batches of up to 10 validate together with automatic bisection of red batches (~log2(N) instead of N revalidations); the manual merge-train is codified as the fallback runbook in `docs/ops/MERGE_TRAIN.md` with the freeze/cross-session guardrails diff --git a/changelog.d/maintenance/npm-staged-publishing.md b/changelog.d/maintenance/npm-staged-publishing.md new file mode 100644 index 0000000000..e57f471083 --- /dev/null +++ b/changelog.d/maintenance/npm-staged-publishing.md @@ -0,0 +1 @@ +- **Release**: npm publishing is now STAGED by default — the workflow boots the packed tarball (`check:pack-boot`) and runs `npm stage publish` (bytes parked on the registry, not installable); the owner verifies the staged bytes and releases them with `npm stage approve` + 2FA, moving the human gate to after the proof (the structural fix for the #7065 broken-tarball class); `publish_mode=direct` remains as a documented emergency fallback diff --git a/changelog.d/maintenance/preflight-v3847-2026-07-13.md b/changelog.d/maintenance/preflight-v3847-2026-07-13.md new file mode 100644 index 0000000000..ce4e9b6897 --- /dev/null +++ b/changelog.d/maintenance/preflight-v3847-2026-07-13.md @@ -0,0 +1 @@ +- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 diff --git a/changelog.d/maintenance/quality-policies-ws5.md b/changelog.d/maintenance/quality-policies-ws5.md new file mode 100644 index 0000000000..990c7e7c20 --- /dev/null +++ b/changelog.d/maintenance/quality-policies-ws5.md @@ -0,0 +1 @@ +- **Docs**: `QUALITY_GATES.md` now codifies the per-runner test retry policy (Playwright 1 CI retry with trace; Vitest per-test explicit quarantine only; node:test never) with target flake SLOs, and the release-level ratchet-drift rule (combination drift on the pure tip is the release captain's to fix once on the branch — never pushed onto contributor PRs, never rebaselined per-PR) diff --git a/changelog.d/maintenance/readme-strategy-tool-counts.md b/changelog.d/maintenance/readme-strategy-tool-counts.md new file mode 100644 index 0000000000..6e82bd00c1 --- /dev/null +++ b/changelog.d/maintenance/readme-strategy-tool-counts.md @@ -0,0 +1 @@ +- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. diff --git a/changelog.d/maintenance/release-green-continuous.md b/changelog.d/maintenance/release-green-continuous.md new file mode 100644 index 0000000000..b3c181510d --- /dev/null +++ b/changelog.d/maintenance/release-green-continuous.md @@ -0,0 +1 @@ +- **CI**: release-green validation is now continuous — every code push to `release/v*` (including the captain's direct sync-back pushes, the one previously ungated write path) triggers a `--quick` HARD-gate run with per-branch superseded-run cancellation, and the tracking issue names the offending push range; the deep `--with-build --full-ci` sweep now runs 3×/day instead of nightly-only (base-red MTTD: ≤24h → ~15min after the offending push) diff --git a/changelog.d/maintenance/release-v3.8.47-basereds.md b/changelog.d/maintenance/release-v3.8.47-basereds.md new file mode 100644 index 0000000000..5305f83ac7 --- /dev/null +++ b/changelog.d/maintenance/release-v3.8.47-basereds.md @@ -0,0 +1 @@ +- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). diff --git a/changelog.d/maintenance/root-cleanup-llmtxt-design-system.md b/changelog.d/maintenance/root-cleanup-llmtxt-design-system.md new file mode 100644 index 0000000000..acaad759f6 --- /dev/null +++ b/changelog.d/maintenance/root-cleanup-llmtxt-design-system.md @@ -0,0 +1 @@ +- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. diff --git a/changelog.d/maintenance/runner-janitor.md b/changelog.d/maintenance/runner-janitor.md new file mode 100644 index 0000000000..365f4a5b00 --- /dev/null +++ b/changelog.d/maintenance/runner-janitor.md @@ -0,0 +1 @@ +- **Ops**: `scripts/ops/runner-janitor.sh` + `docs/ops/RUNNER_BOX.md` codify the self-hosted runner box hygiene that was manual discipline — 30min cron sweeping stale runner temp dirs, alerting at ≥85% disk, and enforcing the proven 4-runner ceiling on the 16 GB box (8-wide OOM-killed jobs twice on the v3.8.47 release day) diff --git a/changelog.d/maintenance/scrub-boundary-test-creds.md b/changelog.d/maintenance/scrub-boundary-test-creds.md new file mode 100644 index 0000000000..e054292504 --- /dev/null +++ b/changelog.d/maintenance/scrub-boundary-test-creds.md @@ -0,0 +1 @@ +- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. diff --git a/changelog.d/maintenance/selfref-6634-shallow.md b/changelog.d/maintenance/selfref-6634-shallow.md new file mode 100644 index 0000000000..6f0c1b345a --- /dev/null +++ b/changelog.d/maintenance/selfref-6634-shallow.md @@ -0,0 +1 @@ +- **Tests**: the #6634 self-reference test now fetches `origin/main` on demand and skips cleanly when the ref is unreachable — it failed as a false positive on shallow/single-ref checkouts (GitHub-hosted runners) diff --git a/changelog.d/maintenance/sync-back-green-gate.md b/changelog.d/maintenance/sync-back-green-gate.md new file mode 100644 index 0000000000..40bded1502 --- /dev/null +++ b/changelog.d/maintenance/sync-back-green-gate.md @@ -0,0 +1 @@ +- **Release tooling**: `sync-next-cycle.mjs` now runs `validate-release-green --quick` on the merged tree BEFORE pushing the parallel-cycle sync-back — the one write path to the release branch that had no CI gate; a red merged tree stays local instead of turning the whole PR queue red (`--skip-green-gate` is the documented emergency hatch) diff --git a/changelog.d/maintenance/tighten-coverage-baseline.md b/changelog.d/maintenance/tighten-coverage-baseline.md new file mode 100644 index 0000000000..119eb1c856 --- /dev/null +++ b/changelog.d/maintenance/tighten-coverage-baseline.md @@ -0,0 +1 @@ +- **chore(quality):** tighten the coverage ratchet to the CI's real numbers (branches 73→78.1, statements/lines 76.5→80.8, functions 82→86.44, plus 7 per-module floors). The gate had been asking for this in plain text; the values come from the merged-coverage run on `main`, not a local run (local measures ~68% vs CI's ~80% — the baseline's own note warns about that gap). diff --git a/changelog.d/maintenance/trunk-flaky-uploads.md b/changelog.d/maintenance/trunk-flaky-uploads.md new file mode 100644 index 0000000000..89c0e6b853 --- /dev/null +++ b/changelog.d/maintenance/trunk-flaky-uploads.md @@ -0,0 +1 @@ +- **CI**: Playwright E2E and both vitest suites now emit JUnit and upload to Trunk Flaky Tests (org `omniroute`) — advisory step, own-origin only, uploader action SHA-pinned (WS5.2/5.3 of the quality plan; node:test stays out of the first wave) diff --git a/changelog.d/maintenance/trunk-upload-fastpath.md b/changelog.d/maintenance/trunk-upload-fastpath.md new file mode 100644 index 0000000000..441f1300fe --- /dev/null +++ b/changelog.d/maintenance/trunk-upload-fastpath.md @@ -0,0 +1 @@ +- **CI**: the fast-path Vitest job (every PR) now also emits JUnit and uploads to Trunk Flaky Tests — the heavy-gate uploads alone (release PR only) would never accumulate flaky-detection volume diff --git a/changelog.d/maintenance/ts7-shadow-typecheck.md b/changelog.d/maintenance/ts7-shadow-typecheck.md new file mode 100644 index 0000000000..e73b70f8de --- /dev/null +++ b/changelog.d/maintenance/ts7-shadow-typecheck.md @@ -0,0 +1 @@ +- **CI**: TypeScript 7 (native compiler, GA 2026-07-08) now runs as an advisory SHADOW of the blocking `typecheck:core` gate on the fast path — same tsconfig, isolated `npx` (no dependency change; the Compiler API only lands in TS 7.1, so typescript-eslint/type-coverage/Stryker stay on 6.x). Local parity proven (0 errors on both, exit 0); promotion to the blocking gate after ~1 week of CI parity diff --git a/changelog.d/maintenance/verify-published.md b/changelog.d/maintenance/verify-published.md new file mode 100644 index 0000000000..3f244bfd73 --- /dev/null +++ b/changelog.d/maintenance/verify-published.md @@ -0,0 +1 @@ +- **Release tooling**: new `scripts/release/verify-published.mjs ` — post-publish net that installs the published version from the public registry inside a clean `node:24-slim` container and boots it until `/api/monitoring/health` reports the expected version (validates the exact bytes users install, on a machine with no repo/devbox state); wired into the release Phase 4 monitoring playbook diff --git a/changelog.d/maintenance/vitest-ui-blocking.md b/changelog.d/maintenance/vitest-ui-blocking.md new file mode 100644 index 0000000000..4ed70f7f3c --- /dev/null +++ b/changelog.d/maintenance/vitest-ui-blocking.md @@ -0,0 +1 @@ +- **CI**: promote `test:vitest:ui` to a blocking gate — the suite is 870/870 green again after the WS6.1 triage (#7127), so `continue-on-error` is removed from the vitest job diff --git a/changelog.d/maintenance/vitest-ui-suite-green.md b/changelog.d/maintenance/vitest-ui-suite-green.md new file mode 100644 index 0000000000..539058bd72 --- /dev/null +++ b/changelog.d/maintenance/vitest-ui-suite-green.md @@ -0,0 +1 @@ +- chore(tests): fix all 70 failing `test:vitest:ui` tests across 30 files (was advisory/parked) — root causes were 15 node:test-authored `.tsx` files never collected by vitest, a missing `window.matchMedia` jsdom polyfill, stale assertions against a redesigned BuildTab wizard / CompressionHub Phase-2 UI, and one obsolete test for a retired Plans screen; suite is now 158/158 files, 870/870 tests green (promotion to blocking is a follow-up) diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000000..6f6d05d0f5 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,19 @@ +# Codecov — WS5.6/D7 of the v3.8.49 quality/velocity plan. +# Philosophy: strict patch, lenient project — the project floor/ratchet already +# lives in quality-baseline.json + the c8 60% gate; Codecov adds the DIFF view +# ("new lines in this PR are covered"), which the global ratchet cannot see. +# INFORMATIONAL during calibration: nothing here blocks a PR. Promote by flipping +# informational to false after ~2 weeks without false blocks (owner decision). +coverage: + status: + project: + default: + informational: true + patch: + default: + target: 70% + informational: true +comment: + layout: "condensed_header, diff" + behavior: default + require_changes: true diff --git a/config/quality/dashboard-typecheck-baseline.json b/config/quality/dashboard-typecheck-baseline.json new file mode 100644 index 0000000000..4f7857d6ce --- /dev/null +++ b/config/quality/dashboard-typecheck-baseline.json @@ -0,0 +1,259 @@ +{ + "open-sse/services/payloadRules.ts": { + "TS2677": 1 + }, + "src/app/(dashboard)/dashboard/HomePageClient.tsx": { + "TS2339": 16 + }, + "src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient.tsx": { + "TS2503": 3 + }, + "src/app/(dashboard)/dashboard/agent-skills/components/CoverageBar.tsx": { + "TS2503": 1 + }, + "src/app/(dashboard)/dashboard/agent-skills/components/McpA2aLinksBar.tsx": { + "TS2503": 2 + }, + "src/app/(dashboard)/dashboard/agent-skills/components/SkillCard.tsx": { + "TS2503": 1 + }, + "src/app/(dashboard)/dashboard/agent-skills/components/SkillPreviewPane.tsx": { + "TS2503": 2 + }, + "src/app/(dashboard)/dashboard/cache/__tests__/CachePage.test.tsx": { + "TS2305": 3, + "TS1117": 1 + }, + "src/app/(dashboard)/dashboard/cache/__tests__/CachePerformance.test.tsx": { + "TS2305": 1, + "TS2322": 2 + }, + "src/app/(dashboard)/dashboard/cache/__tests__/CacheTrends.test.tsx": { + "TS2305": 1, + "TS2322": 6 + }, + "src/app/(dashboard)/dashboard/cache/__tests__/IdempotencyLayer.test.tsx": { + "TS2305": 1 + }, + "src/app/(dashboard)/dashboard/cache/__tests__/MemoryCards.test.tsx": { + "TS2305": 1, + "TS2322": 1 + }, + "src/app/(dashboard)/dashboard/cache/components/CachePerformance.tsx": { + "TS2339": 1 + }, + "src/app/(dashboard)/dashboard/cli-code/components/ClaudeToolCard.tsx": { + "TS2339": 2 + }, + "src/app/(dashboard)/dashboard/cli-code/components/CodexToolCard.tsx": { + "TS2345": 3 + }, + "src/app/(dashboard)/dashboard/cli-code/components/CustomCliCard.tsx": { + "TS2345": 1 + }, + "src/app/(dashboard)/dashboard/cli-code/components/DroidToolCard.tsx": { + "TS2554": 2 + }, + "src/app/(dashboard)/dashboard/combos/page.tsx": { + "TS2339": 4, + "TS2345": 5, + "TS2698": 1, + "TS2322": 13 + }, + "src/app/(dashboard)/dashboard/compression/studio/EncoderComparisonTable.tsx": { + "TS2322": 1 + }, + "src/app/(dashboard)/dashboard/costs/CostOverviewTab.tsx": { + "TS2304": 1 + }, + "src/app/(dashboard)/dashboard/costs/quota-share/QuotaSharePageClient.tsx": { + "TS2551": 7, + "TS2322": 2, + "TS2719": 2, + "TS2739": 1 + }, + "src/app/(dashboard)/dashboard/costs/quota-share/components/StackedAllocationBar.tsx": { + "TS2503": 1 + }, + "src/app/(dashboard)/dashboard/costs/quota-share/components/UsageLogCard.tsx": { + "TS2869": 2 + }, + "src/app/(dashboard)/dashboard/discovery/__tests__/DiscoveryPageClient.test.tsx": { + "TS2305": 2 + }, + "src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx": { + "TS2322": 18 + }, + "src/app/(dashboard)/dashboard/memory/components/tabs/MemoriesTab.tsx": { + "TS2322": 1 + }, + "src/app/(dashboard)/dashboard/omni-skills/OmniSkillsPageClient.tsx": { + "TS2503": 1 + }, + "src/app/(dashboard)/dashboard/omni-skills/components/OmniExecutionsTab.tsx": { + "TS2503": 1 + }, + "src/app/(dashboard)/dashboard/omni-skills/components/OmniMarketplaceTab.tsx": { + "TS2503": 1 + }, + "src/app/(dashboard)/dashboard/omni-skills/components/OmniSandboxTab.tsx": { + "TS2503": 1 + }, + "src/app/(dashboard)/dashboard/omni-skills/components/OmniSkillCard.tsx": { + "TS2503": 1 + }, + "src/app/(dashboard)/dashboard/omni-skills/components/OmniSkillsList.tsx": { + "TS2503": 1 + }, + "src/app/(dashboard)/dashboard/omni-skills/components/SkillInspectorPane.tsx": { + "TS2503": 1 + }, + "src/app/(dashboard)/dashboard/playground/components/PresetPicker.tsx": { + "TS2352": 1 + }, + "src/app/(dashboard)/dashboard/playground/components/ToolsBuilder.tsx": { + "TS2339": 1 + }, + "src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx": { + "TS2322": 4 + }, + "src/app/(dashboard)/dashboard/providers/[id]/__tests__/phase1e.test.tsx": { + "TS2741": 2 + }, + "src/app/(dashboard)/dashboard/providers/[id]/components/CompatibleModelsSection.tsx": { + "TS2741": 1 + }, + "src/app/(dashboard)/dashboard/providers/[id]/components/ConnectionRow.tsx": { + "TS2345": 3, + "TS2322": 1 + }, + "src/app/(dashboard)/dashboard/providers/[id]/components/ConnectionsListPanel.tsx": { + "TS2322": 2 + }, + "src/app/(dashboard)/dashboard/providers/[id]/components/CustomModelsSection.tsx": { + "TS2739": 1 + }, + "src/app/(dashboard)/dashboard/providers/[id]/components/ModelCompatPopover.tsx": { + "TS2304": 5 + }, + "src/app/(dashboard)/dashboard/providers/[id]/components/ProviderModalsPanel.tsx": { + "TS2322": 3, + "TS2739": 1, + "TS2345": 3 + }, + "src/app/(dashboard)/dashboard/providers/[id]/components/ProviderModelsSection.tsx": { + "TS2322": 1 + }, + "src/app/(dashboard)/dashboard/providers/[id]/components/ProviderParamFilterSection.tsx": { + "TS2339": 6 + }, + "src/app/(dashboard)/dashboard/providers/[id]/components/ProviderPlaygroundPanel.tsx": { + "TS2503": 1 + }, + "src/app/(dashboard)/dashboard/providers/[id]/components/__tests__/phase1d.test.tsx": { + "TS2739": 2 + }, + "src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx": { + "TS2322": 1 + }, + "src/app/(dashboard)/dashboard/providers/[id]/hooks/useModelImportHandlers.ts": { + "TS2339": 1 + }, + "src/app/(dashboard)/dashboard/providers/[id]/hooks/useModelVisibilityHandlers.ts": { + "TS2339": 15 + }, + "src/app/(dashboard)/dashboard/providers/components/onboarding/providerOnboardingCatalog.ts": { + "TS2339": 4, + "TS2345": 2 + }, + "src/app/(dashboard)/dashboard/providers/providerPageUtils.ts": { + "TS2345": 1 + }, + "src/app/(dashboard)/dashboard/providers/services/components/CliproxyModelMappingEditor.tsx": { + "TS2339": 1 + }, + "src/app/(dashboard)/dashboard/quota/page.tsx": { + "TS2339": 4 + }, + "src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx": { + "TS2304": 1 + }, + "src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx": { + "TS2339": 4 + }, + "src/app/(dashboard)/dashboard/settings/components/RedisLauncherPanel.tsx": { + "TS2345": 11 + }, + "src/app/(dashboard)/dashboard/settings/components/SidebarTab.tsx": { + "TS2322": 1 + }, + "src/app/(dashboard)/dashboard/settings/components/proxy/FreePoolTab.tsx": { + "TS2304": 1 + }, + "src/app/(dashboard)/dashboard/tools/traffic-inspector/components/CustomHostsManager.tsx": { + "TS2339": 1 + }, + "src/app/(dashboard)/dashboard/tools/traffic-inspector/components/tabs/ResponseBodyTab.tsx": { + "TS2339": 5 + }, + "src/app/(dashboard)/dashboard/translator/components/MonitorTab.tsx": { + "TS2345": 1 + }, + "src/app/(dashboard)/dashboard/translator/components/advanced/CompressionPreviewAccordion.tsx": { + "TS4104": 1 + }, + "src/app/(dashboard)/dashboard/translator/components/advanced/StreamTransformerAccordion.tsx": { + "TS2345": 1 + }, + "src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx": { + "TS2339": 2 + }, + "src/app/(dashboard)/dashboard/usage/components/ProviderLimits/parts/QuotaEnvGroup.tsx": { + "TS2739": 1 + }, + "src/lib/combos/builderDraft.ts": { + "TS2741": 1 + }, + "src/lib/providers/codexFastTier.ts": { + "TS2367": 1 + }, + "src/lib/services/htmlRewriter.ts": { + "TS2322": 2, + "TS2345": 2 + }, + "src/mitm/inspector/sseMerger.ts": { + "TS2352": 1 + }, + "src/shared/components/Header.tsx": { + "TS2353": 1 + }, + "src/shared/components/MonacoEditor.tsx": { + "TS2307": 1 + }, + "src/shared/components/OAuthModal.tsx": { + "TS2769": 4, + "TS2345": 4 + }, + "src/shared/components/SkillsConceptCard.tsx": { + "TS2503": 1 + }, + "src/shared/components/analytics/charts.tsx": { + "TS2345": 1 + }, + "src/shared/components/analytics/rechartsDonuts.tsx": { + "TS2739": 2 + }, + "src/shared/hooks/useElectron.ts": { + "TS2339": 19 + }, + "src/shared/providers/webSessionCredentials.ts": { + "TS2353": 1, + "TS2322": 1 + }, + "src/shared/schemas/cliCatalog.ts": { + "TS2554": 2 + }, + "src/shared/services/opencodeConfig.ts": { + "TS2345": 1 + } +} diff --git a/config/quality/dependency-allowlist.json b/config/quality/dependency-allowlist.json index 6dae8b6274..5ecfb4329a 100644 --- a/config/quality/dependency-allowlist.json +++ b/config/quality/dependency-allowlist.json @@ -45,6 +45,7 @@ "concurrently", "cross-env", "csv-stringify", + "ctrf", "dompurify", "dpdm", "electron", @@ -63,6 +64,7 @@ "glob", "http-proxy-middleware", "https-proxy-agent", + "httpyac", "husky", "ink", "ink-spinner", @@ -74,6 +76,7 @@ "jscpd", "jsdom", "jsonc-parser", + "junit-to-ctrf", "keytar", "knip", "license-checker-rseidelsohn", @@ -99,7 +102,9 @@ "pino-abstract-transport", "pino-pretty", "playwright", + "playwright-ctrf-json-reporter", "prettier", + "promptfoo", "react", "react-dom", "react-is", diff --git a/config/quality/e2e-timings.json b/config/quality/e2e-timings.json new file mode 100644 index 0000000000..77571fcbf9 --- /dev/null +++ b/config/quality/e2e-timings.json @@ -0,0 +1,39 @@ +{ + "_meta": "Relative weights for scripts/quality/balance-e2e-shards.mjs (LPT shard packing). Unitless \u2014 only ratios matter. Seeded from spec LOC (proxy) on 2026-07-13; replace values with real per-file durations (seconds) from a full CI run's reports whenever convenient. New specs without an entry get the median weight.", + "a11y-resilience.spec.ts": 59, + "a11y.spec.ts": 281, + "agent-bridge-traffic-cross.spec.ts": 155, + "agent-bridge.spec.ts": 160, + "agent-skills-page.spec.ts": 205, + "analytics-tabs.spec.ts": 334, + "api-keys-flow.spec.ts": 643, + "api.spec.ts": 30, + "combo-live-studio.spec.ts": 35, + "combo-unification.spec.ts": 192, + "combos-flow.spec.ts": 629, + "compression-studio.spec.ts": 55, + "error-pages.spec.ts": 101, + "group-b-activity-feed.spec.ts": 96, + "group-b-quota-plans-config.spec.ts": 154, + "group-b-quota-share-pools.spec.ts": 98, + "group-b-redirect-logs-activity.spec.ts": 56, + "memory-engine.spec.ts": 607, + "memory-qdrant-routes.spec.ts": 360, + "memory-settings.spec.ts": 193, + "navigation.spec.ts": 28, + "playground-compare.spec.ts": 138, + "playground-studio.spec.ts": 101, + "protocol-visibility.spec.ts": 25, + "providers-bailian-coding-plan.spec.ts": 240, + "providers-management.spec.ts": 324, + "proxy-registry.smoke.spec.ts": 218, + "resilience-plan-alignment.spec.ts": 382, + "responsive.spec.ts": 21, + "search-tools-studio.spec.ts": 133, + "settings-toggles.spec.ts": 127, + "skills-marketplace.spec.ts": 209, + "smoke.spec.ts": 33, + "traffic-inspector.spec.ts": 212, + "translator-friendly.spec.ts": 115, + "visual-resilience-smoke.spec.ts": 20 +} \ No newline at end of file diff --git a/config/quality/eslint-suppressions.json b/config/quality/eslint-suppressions.json index dcea92f2a9..7d84d77ebb 100644 --- a/config/quality/eslint-suppressions.json +++ b/config/quality/eslint-suppressions.json @@ -1802,11 +1802,6 @@ "count": 6 } }, - "tests/unit/provider-scoped-models-route.test.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 7 - } - }, "tests/unit/provider-validation-hardening.test.ts": { "@typescript-eslint/no-explicit-any": { "count": 6 @@ -2327,4 +2322,4 @@ "count": 5 } } -} +} \ No newline at end of file diff --git a/config/quality/quality-baseline.json b/config/quality/quality-baseline.json index 943b0fa4b8..ee2c81c6d3 100644 --- a/config/quality/quality-baseline.json +++ b/config/quality/quality-baseline.json @@ -96,7 +96,7 @@ "tightenSlack": 10 }, "openapiCoverage.pct": { - "value": 38.0, + "value": 38, "direction": "up", "eps": 0.5, "_tighten_2026_07_04_v3844_release": "36.9 -> 39.3 (aperto exigido pelo --require-tighten no PR de release #5925). A cobertura OpenAPI melhorou no ciclo (9 rotas documentadas em 8fb020676 + as rotas novas de #5939/#5817/#6034/#5998 documentadas junto das features). 39.3 = valor medido pelo CI Quality Ratchet no run 28708141003 (tip 00c55afcb).", @@ -158,12 +158,14 @@ "dedicatedGate": true }, "secretFindings": { - "value": 3, + "_note": "Zeroed 2026-07-13 (WS6/D3): the 3 frozen generic-api-key FPs are allowlisted with justification in .gitleaks.toml — any NEW finding regresses the ratchet.", + "value": 0, "direction": "down", "dedicatedGate": true }, "zizmorFindings": { - "value": 169, + "value": 175, + "_rebaseline_2026_07_17_v3849_release": "169 -> 175 (+6). Cycle workflow drift (v3.8.48/v3.8.49): npm-publish.yml (new, WS1.3 #7092), electron-release.yml, nightly-compat.yml, nightly-release-green.yml, CI restructures (#7501 full-history base fetch, #7355 main-green, #7202 merge-queue gates, Trunk/Codecov). Breakdown vs v3.8.47: +3 unpinned-uses (@vN convention, deliberate per _scanner_harden_workflows_2026_06_16), +2 cache-poisoning (artifact upload/cache in the OWN electron-release/npm-publish RELEASE workflows -- operator-controlled, not fork-PR exploitable), +1 excessive-permissions (nightly-compat.yml permissions:issues). No new template-injection/artipacked/dangerous-triggers. Measured with zizmor 1.25.2 via `node scripts/check/check-workflows.mjs --ratchet` = 175 on da3a0be69.", "direction": "down", "dedicatedGate": true, "_rebaseline_2026_06_23_fastpath_gates": "155 -> 159 (+4). Two new jobs added to .github/workflows/quality.yml (fast-vitest, fast-unit) to run vitest + the full unit suite on the PR->release fast-path (release-acceleration plan, _tasks/release-bench/v3.8.35/PLANO-IMPLEMENTACAO.md). The +4 are unpinned-uses: actions/checkout@v7 + actions/setup-node@v6 in each of the 2 jobs — the SAME deliberate @vN convention as every other workflow (see _scanner_harden_workflows_2026_06_16). SHA-pinning only these would violate the convention. No new template-injection/artipacked/cache-poisoning. Measured locally via `npm run check:workflows -- --ratchet` = 159.", @@ -187,12 +189,6 @@ "dedicatedGate": true, "_note": "oasdiff breaking-change gate (Fase 9 Onda 0). Blocks any breaking change vs base spec." }, - "semgrepFindings": { - "value": 0, - "direction": "down", - "dedicatedGate": true, - "_note": "semgrep owasp/secrets findings. ADVISORY until first CI value is frozen, then flip blocking (Fase 9)." - }, "mutationScore.src/sse/services/auth.ts": { "value": 52.57, "direction": "up", diff --git a/docs/architecture/QUALITY_GATES.md b/docs/architecture/QUALITY_GATES.md index 3686b9eef4..5fa94abd27 100644 --- a/docs/architecture/QUALITY_GATES.md +++ b/docs/architecture/QUALITY_GATES.md @@ -46,6 +46,7 @@ Runs on every PR to `main`. Blocks merge on failure. | `check:docs-sync` | CHANGELOG version, OpenAPI version, and `llm.txt` are in sync | Yes | | `typecheck:core` | TypeScript compilation without errors (advisory warnings only) | Yes | | `typecheck:noimplicit:core` | Strict `noImplicitAny` — forward-looking; many pre-existing call sites still need annotations | **Advisory** (`continue-on-error: true`) | +| `check:dashboard-typecheck` | `tsc` scoped to `src/app/(dashboard)/**` (#7033) — `typecheck:core`'s curated 27-file allowlist does not include any dashboard TSX, and `next build` never type-checks it either (`next.config.mjs` sets `ignoreBuildErrors: true`), so orphaned-identifier regressions there (#6625/#6909) were invisible to CI. Diffs against a frozen per-file/per-TS-code count baseline (`config/quality/dashboard-typecheck-baseline.json`, same stale-enforcement pattern as `check:known-symbols`) — only NEW errors beyond the baselined count fail the gate; ratchet down with `--update` when a pre-existing error is fixed. | Yes | ### Job: `quality-gate` @@ -173,6 +174,31 @@ pending implementation). --- +## Test Retry Policy (WS5.4, v3.8.49) + +Retry is per-runner, never a global blanket — a blanket retry converts real regressions +into invisible flakes: + +| Runner | Policy | Why | +| --- | --- | --- | +| Playwright (e2e) | `retries: 1` in CI only, with `trace: on-first-retry` | Browser/network timing is genuinely nondeterministic; one retry with a trace turns a flake into a diagnosable artifact | +| Vitest | NO global retry. A proven-flaky test gets an explicit per-test retry (visible in the diff, reviewed in PR) | Keeps the quarantine list in the repo, never opaque | +| node:test (unit) | NO retry, ever | A flaky unit test is a bug in the test — fix it, don't re-roll it | + +Target SLOs once flake telemetry lands (WS5.2/5.3): <1% flake rate per test +("fix now" threshold), ≥95% pass rate per pipeline. Industry reference values — +recalibrate against our own measurements. + +## Release-Level Ratchet Drift (WS5.5, v3.8.49) + +When a ratchet (file-size, complexity, eslint warnings) regresses on the PURE release +tip — i.e. the COMBINATION of merges regressed it, and no single PR reproduces the +regression on its own branch — the fix belongs to the **release captain, once, on the +release branch**: prefer extraction/refactor; rebaseline only with the documented +justification entry. Never push combination drift onto a contributor PR, and never +rebaseline per-PR (that hides real regressions). Discriminate first: reproduce the +red against the pure tip in a probe worktree before assuming your PR caused it. + ## Allowlist Policy Every gate that cannot fail on pre-existing violations uses a frozen allowlist diff --git a/docs/compression/COMPRESSION_GUIDE.md b/docs/compression/COMPRESSION_GUIDE.md index 472cf36b61..48bf546093 100644 --- a/docs/compression/COMPRESSION_GUIDE.md +++ b/docs/compression/COMPRESSION_GUIDE.md @@ -278,7 +278,8 @@ Every compressed request includes stats in the server logs: | Phase 1 | Off, Lite | ✅ Shipped | | Phase 2 | Standard, Aggressive, Ultra | ✅ Shipped | | Phase 3 | RTK, Stacked, Compression Combos | ✅ Shipped | -| Phase 4 | Output Styles, SLM-tier Ultra, adaptive context-budget, eval harness | ✅ Shipped | +| Phase 4 | Output Styles, SLM-tier Ultra, eval harness | ✅ Shipped | +| Phase 4C | Adaptive context-budget ("dial") — compute engine + API (`contextBudget` on `PUT /api/settings/compression`) | ✅ Shipped (API-configurable; dashboard controls not yet built, #7005) | --- diff --git a/docs/i18n/ar/CHANGELOG.md b/docs/i18n/ar/CHANGELOG.md index f855817c21..9046b606ca 100644 --- a/docs/i18n/ar/CHANGELOG.md +++ b/docs/i18n/ar/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/az/CHANGELOG.md b/docs/i18n/az/CHANGELOG.md index 850d934ee8..5fbb32038e 100644 --- a/docs/i18n/az/CHANGELOG.md +++ b/docs/i18n/az/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/bg/CHANGELOG.md b/docs/i18n/bg/CHANGELOG.md index 850d934ee8..5fbb32038e 100644 --- a/docs/i18n/bg/CHANGELOG.md +++ b/docs/i18n/bg/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/bn/CHANGELOG.md b/docs/i18n/bn/CHANGELOG.md index 971594fb3e..1ae97f7f57 100644 --- a/docs/i18n/bn/CHANGELOG.md +++ b/docs/i18n/bn/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/cs/CHANGELOG.md b/docs/i18n/cs/CHANGELOG.md index 8c0a63a8b0..4052cf7134 100644 --- a/docs/i18n/cs/CHANGELOG.md +++ b/docs/i18n/cs/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/da/CHANGELOG.md b/docs/i18n/da/CHANGELOG.md index 676f77bd65..19e3353f39 100644 --- a/docs/i18n/da/CHANGELOG.md +++ b/docs/i18n/da/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/de/CHANGELOG.md b/docs/i18n/de/CHANGELOG.md index cd0531b473..ae4274c0e5 100644 --- a/docs/i18n/de/CHANGELOG.md +++ b/docs/i18n/de/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/es/CHANGELOG.md b/docs/i18n/es/CHANGELOG.md index d798798dae..712d2f9b5d 100644 --- a/docs/i18n/es/CHANGELOG.md +++ b/docs/i18n/es/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/fa/CHANGELOG.md b/docs/i18n/fa/CHANGELOG.md index 7ae4dfa156..faa179cd94 100644 --- a/docs/i18n/fa/CHANGELOG.md +++ b/docs/i18n/fa/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/fi/CHANGELOG.md b/docs/i18n/fi/CHANGELOG.md index 2582c446b3..402b114cff 100644 --- a/docs/i18n/fi/CHANGELOG.md +++ b/docs/i18n/fi/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/fr/CHANGELOG.md b/docs/i18n/fr/CHANGELOG.md index 0d07715a5b..605c4a2037 100644 --- a/docs/i18n/fr/CHANGELOG.md +++ b/docs/i18n/fr/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/gu/CHANGELOG.md b/docs/i18n/gu/CHANGELOG.md index a48f448d54..641b35a387 100644 --- a/docs/i18n/gu/CHANGELOG.md +++ b/docs/i18n/gu/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/he/CHANGELOG.md b/docs/i18n/he/CHANGELOG.md index 7553ce420d..629a1b403e 100644 --- a/docs/i18n/he/CHANGELOG.md +++ b/docs/i18n/he/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/hi/CHANGELOG.md b/docs/i18n/hi/CHANGELOG.md index 93f996a33f..8a55198736 100644 --- a/docs/i18n/hi/CHANGELOG.md +++ b/docs/i18n/hi/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/hu/CHANGELOG.md b/docs/i18n/hu/CHANGELOG.md index 3d4ccba9ec..398dfe68eb 100644 --- a/docs/i18n/hu/CHANGELOG.md +++ b/docs/i18n/hu/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/id/CHANGELOG.md b/docs/i18n/id/CHANGELOG.md index a6e1681047..3036ea0dda 100644 --- a/docs/i18n/id/CHANGELOG.md +++ b/docs/i18n/id/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/in/CHANGELOG.md b/docs/i18n/in/CHANGELOG.md index d70b15d396..e18e61ca7f 100644 --- a/docs/i18n/in/CHANGELOG.md +++ b/docs/i18n/in/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/it/CHANGELOG.md b/docs/i18n/it/CHANGELOG.md index 23afdbdd5a..fda069bbed 100644 --- a/docs/i18n/it/CHANGELOG.md +++ b/docs/i18n/it/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/ja/CHANGELOG.md b/docs/i18n/ja/CHANGELOG.md index aeaff95477..967105b3a2 100644 --- a/docs/i18n/ja/CHANGELOG.md +++ b/docs/i18n/ja/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/ko/CHANGELOG.md b/docs/i18n/ko/CHANGELOG.md index 4146ca0b71..14c4b4c781 100644 --- a/docs/i18n/ko/CHANGELOG.md +++ b/docs/i18n/ko/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/mr/CHANGELOG.md b/docs/i18n/mr/CHANGELOG.md index 5e4ea96d2f..83e1f423a8 100644 --- a/docs/i18n/mr/CHANGELOG.md +++ b/docs/i18n/mr/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/ms/CHANGELOG.md b/docs/i18n/ms/CHANGELOG.md index c44aaaf20a..85db87ed90 100644 --- a/docs/i18n/ms/CHANGELOG.md +++ b/docs/i18n/ms/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/nl/CHANGELOG.md b/docs/i18n/nl/CHANGELOG.md index 70799c6bbd..b06be5ce83 100644 --- a/docs/i18n/nl/CHANGELOG.md +++ b/docs/i18n/nl/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/no/CHANGELOG.md b/docs/i18n/no/CHANGELOG.md index 5a50396e2a..be3a000cad 100644 --- a/docs/i18n/no/CHANGELOG.md +++ b/docs/i18n/no/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/phi/CHANGELOG.md b/docs/i18n/phi/CHANGELOG.md index 1fb507ccbf..30a927ed54 100644 --- a/docs/i18n/phi/CHANGELOG.md +++ b/docs/i18n/phi/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/pl/CHANGELOG.md b/docs/i18n/pl/CHANGELOG.md index 9f32de4fa4..1c7e92d727 100644 --- a/docs/i18n/pl/CHANGELOG.md +++ b/docs/i18n/pl/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/pt-BR/CHANGELOG.md b/docs/i18n/pt-BR/CHANGELOG.md index 53a2cddafd..5728cb8ee5 100644 --- a/docs/i18n/pt-BR/CHANGELOG.md +++ b/docs/i18n/pt-BR/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/pt/CHANGELOG.md b/docs/i18n/pt/CHANGELOG.md index e432ca97b1..154ddf5cbd 100644 --- a/docs/i18n/pt/CHANGELOG.md +++ b/docs/i18n/pt/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/ro/CHANGELOG.md b/docs/i18n/ro/CHANGELOG.md index 4568fae186..f1ef3329d1 100644 --- a/docs/i18n/ro/CHANGELOG.md +++ b/docs/i18n/ro/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/ru/CHANGELOG.md b/docs/i18n/ru/CHANGELOG.md index d5f2e548f9..fe338f8e87 100644 --- a/docs/i18n/ru/CHANGELOG.md +++ b/docs/i18n/ru/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/sk/CHANGELOG.md b/docs/i18n/sk/CHANGELOG.md index ec5b2f43a6..2d7a53e258 100644 --- a/docs/i18n/sk/CHANGELOG.md +++ b/docs/i18n/sk/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/sv/CHANGELOG.md b/docs/i18n/sv/CHANGELOG.md index eb5dcf7460..5437d7f8bd 100644 --- a/docs/i18n/sv/CHANGELOG.md +++ b/docs/i18n/sv/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/sw/CHANGELOG.md b/docs/i18n/sw/CHANGELOG.md index 0fddf20f9f..94c5b05a79 100644 --- a/docs/i18n/sw/CHANGELOG.md +++ b/docs/i18n/sw/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/ta/CHANGELOG.md b/docs/i18n/ta/CHANGELOG.md index 99db23b0b0..05d979202e 100644 --- a/docs/i18n/ta/CHANGELOG.md +++ b/docs/i18n/ta/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/te/CHANGELOG.md b/docs/i18n/te/CHANGELOG.md index 1f9d75c1c9..1e187b92b9 100644 --- a/docs/i18n/te/CHANGELOG.md +++ b/docs/i18n/te/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/th/CHANGELOG.md b/docs/i18n/th/CHANGELOG.md index 5d5ecf3af6..8483c695d0 100644 --- a/docs/i18n/th/CHANGELOG.md +++ b/docs/i18n/th/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/tr/CHANGELOG.md b/docs/i18n/tr/CHANGELOG.md index b5d641fb6f..c4226be42a 100644 --- a/docs/i18n/tr/CHANGELOG.md +++ b/docs/i18n/tr/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/uk-UA/CHANGELOG.md b/docs/i18n/uk-UA/CHANGELOG.md index b1af97978b..0c1d97be77 100644 --- a/docs/i18n/uk-UA/CHANGELOG.md +++ b/docs/i18n/uk-UA/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/ur/CHANGELOG.md b/docs/i18n/ur/CHANGELOG.md index 6f09b8af09..c640549db7 100644 --- a/docs/i18n/ur/CHANGELOG.md +++ b/docs/i18n/ur/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/vi/CHANGELOG.md b/docs/i18n/vi/CHANGELOG.md index 8a192ad1a1..c7fd87eb08 100644 --- a/docs/i18n/vi/CHANGELOG.md +++ b/docs/i18n/vi/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/zh-CN/CHANGELOG.md b/docs/i18n/zh-CN/CHANGELOG.md index 96bc67e59c..7d70f1a7c0 100644 --- a/docs/i18n/zh-CN/CHANGELOG.md +++ b/docs/i18n/zh-CN/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/zh-CN/docs/reference/ENVIRONMENT.md b/docs/i18n/zh-CN/docs/reference/ENVIRONMENT.md index b97bf07d96..782d23d747 100644 --- a/docs/i18n/zh-CN/docs/reference/ENVIRONMENT.md +++ b/docs/i18n/zh-CN/docs/reference/ENVIRONMENT.md @@ -272,7 +272,7 @@ OmniRoute 提供两层防护:请求侧的注入扫描和响应侧的 PII 脱 | `OMNIROUTE_PUBLIC_URL` | _(未设置)_ | `open-sse/utils/kieTask.ts` | 用于组合异步回调 URL 的公共源。kie.ai 回调的最低优先级回退;也用作其他中继的通用公共 URL。 | | `OMNIROUTE_CROF_USAGE_URL` | `https://crof.ai/usage_api/` | `open-sse/services/usage.ts` | Usage 页面使用的 CrofAI 配额查询端点。可覆盖为中继/测试固定件。 | | `OMNIROUTE_OPENCODE_QUOTA_URL` | `https://opencode.ai/zen/go/v1/quota` | `open-sse/services/opencodeQuotaFetcher.ts` | Usage 页面使用的 OpenCode (zen/go) 配额查询端点。可覆盖为中继/测试固定件。 | -| `OMNIROUTE_OPENCODE_GO_QUOTA_URL` | `https://api.z.ai/api/monitor/usage/quota/limit` | `open-sse/services/usage.ts` | Usage 页面使用的 OpenCode Go 配额查询端点。可覆盖为中继/测试固定件。 | +| `OMNIROUTE_OPENCODE_GO_QUOTA_URL` | _(未设置)_ | `open-sse/services/opencodeOllamaUsage.ts` | Usage 页面使用的 OpenCode Go 配额查询端点。OpenCode Go 没有公开的配额 API,因此没有默认值;除非运维人员显式设置该变量选择接入自建/镜像端点,否则不会发起网络请求。 | | `OMNIROUTE_OPENCODE_GO_DASHBOARD_URL` | `https://opencode.ai/workspace` | `open-sse/services/usage.ts` | 配置了 workspace ID 和 auth Cookie 时用于配额抓取的 OpenCode Go Dashboard 基础 URL。可覆盖为中继/测试固定件。 | | `OPENCODE_GO_WORKSPACE_ID` | _(未设置)_ | `open-sse/services/usage.ts` | 用于 Dashboard 配额抓取的 OpenCode Go workspace ID。配置多个账户时,推荐使用每个连接的 Dashboard 字段。 | | `OMNIROUTE_OPENCODE_GO_WORKSPACE_ID` | _(未设置)_ | `open-sse/services/usage.ts` | OpenCode Go workspace ID 环境变量的备选名,在较短的别名之前使用。配置多个账户时,推荐使用每个连接的 Dashboard 字段。 | diff --git a/docs/i18n/zh-TW/CHANGELOG.md b/docs/i18n/zh-TW/CHANGELOG.md index 123cf82a14..cc981d58d8 100644 --- a/docs/i18n/zh-TW/CHANGELOG.md +++ b/docs/i18n/zh-TW/CHANGELOG.md @@ -6,6 +6,10 @@ ## [3.8.31] — 2026-06-20 +## [3.8.49] — TBD + +--- + ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. @@ -19,323 +23,13 @@ --- -## [3.8.47] — 2026-07-13 +## [3.8.47] — TBD _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ -- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). - -### ✨ New Features - -- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - -- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) -- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) -- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) -- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) -- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) -- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) -- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) -- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) -- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - -- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) -- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) -- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) -- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) -- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) -- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) -- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) -- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) -- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) -- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) -- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) -- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. -- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) -- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. -- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). -- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). -- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). -- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) -- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) -- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). -- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). -- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). -- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). -- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). -- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. -- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). -- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). -- **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) -- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. -- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) -- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) -- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) -- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) -- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. -- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. -- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) -- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. -- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. -- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. -- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. -- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. -- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. -- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. -- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. -- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). -- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). -- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) -- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) -- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) -- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. -- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) -- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) -- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) -- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) -- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) -- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) -- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) -- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun -- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). -- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). -- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). -- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) -- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) -- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) -- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) -- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) -- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) -- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). -- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). -- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). -- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) -- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) -- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) -- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). -- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) -- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) -- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). -- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). -- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). -- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) -- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) -- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) -- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) -- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) -- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev -- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. -- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) -- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). -- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). -- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. -- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). -- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. -- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) -- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) -- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. -- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. -- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). -- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. -- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). -- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). -- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). -- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). -- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) -- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). -- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) -- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) -- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo -- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). -- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). -- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). -- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). -- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) -- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) -- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). -- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) -- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) -- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. -- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). -- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). -- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) -- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) -- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). -- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) -- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) -- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) -- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) -- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) -- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) -- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) -- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) -- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) -- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) -- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) -- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) -- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) -- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) -- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl -- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) -- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) -- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) -- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). -- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). - ### 📝 Maintenance -- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) -- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) -- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - -- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) -- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). -- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. -- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. -- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) -- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) -- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract -- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) -- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). -- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). -- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 -- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. -- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). -- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. -- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. - -### 🙌 Contributors - -Thanks to everyone whose work landed in v3.8.47: - -| Contributor | PRs / Issues | -| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | -| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | -| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | -| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | -| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | -| [@artickc](https://github.com/artickc) | #6363, #6763 | -| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | -| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | -| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | -| [@charleszolot](https://github.com/charleszolot) | #6571 | -| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | -| [@chy1211](https://github.com/chy1211) | direct commit / report | -| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | -| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | -| [@developerjillur](https://github.com/developerjillur) | direct commit / report | -| [@eidoog](https://github.com/eidoog) | direct commit / report | -| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | -| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | -| [@growab](https://github.com/growab) | #6867 | -| [@hajilok](https://github.com/hajilok) | #6126, #6833 | -| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | -| [@herjarsa](https://github.com/herjarsa) | direct commit / report | -| [@iamraydoan](https://github.com/iamraydoan) | #6798 | -| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | -| [@itiwant](https://github.com/itiwant) | direct commit / report | -| [@janeza2](https://github.com/janeza2) | #6308 | -| [@JxnLexn](https://github.com/JxnLexn) | #6776 | -| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | -| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | -| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | -| [@lunkerchen](https://github.com/lunkerchen) | #6320 | -| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | -| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | -| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | -| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | -| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | -| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | -| [@quanturbo](https://github.com/quanturbo) | #6780 | -| [@rafaumeu](https://github.com/rafaumeu) | #6813 | -| [@rafpigna](https://github.com/rafpigna) | #6574 | -| [@rucciva](https://github.com/rucciva) | #4125 | -| [@rushsinging](https://github.com/rushsinging) | #6807 | -| [@ryanngit](https://github.com/ryanngit) | direct commit / report | -| [@samir-abis](https://github.com/samir-abis) | direct commit / report | -| [@SeaXen](https://github.com/SeaXen) | direct commit / report | -| [@shabeer](https://github.com/shabeer) | direct commit / report | -| [@Squawk7777](https://github.com/Squawk7777) | #6565 | -| [@strangersp](https://github.com/strangersp) | #6587 | -| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | -| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | -| [@tjengbudi](https://github.com/tjengbudi) | #4009 | -| [@whale9820](https://github.com/whale9820) | direct commit / report | -| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | -| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | -| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | -| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 36d2f06f7a..994154d41d 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: OmniRoute API - version: 3.8.48 + version: 3.8.49 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/docs/ops/HOMOLOGATION.md b/docs/ops/HOMOLOGATION.md new file mode 100644 index 0000000000..2754d12956 --- /dev/null +++ b/docs/ops/HOMOLOGATION.md @@ -0,0 +1,104 @@ +--- +title: "Homologation Suite (npm run homolog)" +version: 3.8.49 +lastUpdated: 2026-07-14 +--- + +# Homologation Suite (`npm run homolog`) + +Real-environment E2E validation of the OmniRoute deploy running on the homologation VPS +(`HOMOLOG_BASE_URL`, e.g. `http://192.168.0.15:20128`). One command replaces the manual +release STOP #2 checklist with an automated, evidence-producing run. + +## What it covers + +| Layer | What it checks | Implementation | +| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | +| L0 — health/parity | `/api/monitoring/health` responds `200` with `status: "healthy"` and the expected version | `scripts/homolog/lib/parity.mjs` | +| L1a — ephemeral key | Admin login → `POST /api/keys` creates a scoped API key for the run, revoked (`DELETE /api/keys/:id`) in a `finally` block regardless of outcome | `scripts/homolog/lib/adminClient.mjs` | +| L1b — API surface | `/v1/models` catalog, a real non-streaming chat completion (tier-critical model, `max_tokens: 5`), an invalid-key `401`, and public `/api/monitoring/health` | `tests/homolog/api/core.http` (httpYac) | +| L1c — SSE streaming | Real streaming chat completion; asserts `text/event-stream`, at least one content delta, and a `[DONE]` terminator | `scripts/homolog/lib/sseCheck.mjs` | +| L2 — real providers | One minimal-cost chat request per critical provider present in the live `/v1/models` catalog, generated on the fly via promptfoo | `scripts/homolog/gen-promptfoo.mjs` + `scripts/homolog/lib/providerTiers.mjs` | +| L4a — UI auth | Logs in once via the real login form and reuses the session (`storageState`) across the UI layer | `tests/homolog/ui/auth.setup.ts` | +| L4b — UI routes | Every static `page.tsx` under `src/app/(dashboard)/dashboard` (discovered from the filesystem, dynamic `[param]` routes skipped) loads without an HTTP error, a page error, or the Next.js error boundary | `tests/homolog/ui/routes.spec.ts` | +| L4c — UI critical flow | Creates an API key through the dashboard UI and revokes it again (leaves no residue on the VPS) | `tests/homolog/ui/api-key-flow.spec.ts` | +| L5 — unified report | Merges httpYac (via `junit-to-ctrf`), the promptfoo→CTRF adapter, and the Playwright CTRF reporter into one `homolog-ctrf.json`, plus a human-readable `homolog-report/summary.md` | `scripts/homolog/run.mjs` | + +Zero LLM involvement in the replay itself — this is a deterministic regression battery, +not an eval. AI only enters in future maintenance work (see Roadmap below). + +## Prerequisites + +1. Copy `.env.homolog.example` to `.env.homolog` (gitignored — never commit it) and fill in: + - `HOMOLOG_BASE_URL` — the target deploy, e.g. `http://192.168.0.15:20128`. + - `HOMOLOG_ADMIN_PASSWORD` — the dashboard management password for that deploy. + - `HOMOLOG_CRITICAL_PROVIDERS` — comma-separated provider prefixes that get a real + smoke chat request (e.g. `openai,anthropic,gemini,codex,grok,glm,deepseek,openrouter`). + - `HOMOLOG_API_KEY` — leave empty in normal runs; the suite creates and revokes its + own ephemeral key. Only set this to debug a single layer in isolation. +2. `npm install` in the repo (the suite's dependencies — `httpyac`, `promptfoo`, + `playwright-ctrf-json-reporter`, `junit-to-ctrf`, `ctrf` — are regular devDependencies). +3. `npx playwright install` if the browser binaries are not already present. + +## How to run + +```bash +npm run homolog +``` + +To validate against a deploy whose version does not match the local `package.json` +(e.g. a homologation box still on a previous patch release), override the expected +version explicitly: + +```bash +HOMOLOG_EXPECT_VERSION=3.8.47 npm run homolog +``` + +The run exits non-zero if any layer fails, and always attempts to revoke the ephemeral +API key it created, even on failure (`finally` block in `scripts/homolog/run.mjs`). + +## Reading the report + +All output lands in `homolog-report/` (gitignored): + +- `summary.md` — the same table printed to stdout, one row per layer (✅/❌ + detail). +- `homolog-ctrf.json` — the unified CTRF report (merge of API/SSE, provider-smoke, and + UI results) — this is the artifact to attach to a release STOP #2 checklist. +- `httpyac-junit.xml`, `api-ctrf.json`, `providers-ctrf.json`, `ui-ctrf.json` — the + per-layer raw/intermediate reports. +- `promptfooconfig.yaml`, `provider-misses.json` — the generated promptfoo config for + the current run and any critical providers that were missing from the live catalog. + +A failing L0 aborts immediately (no ephemeral key is created) since a version/health +mismatch means every downstream layer would be validating the wrong deploy. + +## Re-baselining when the UI changes legitimately + +L4b (route smoke) and L4c (API-key UI flow) are driven by real DOM locators, not +snapshots, so most legitimate UI changes do not require any suite update. When a change +does break a locator (e.g. a renamed button label or a moved settings page): + +1. Re-confirm the locator against the current source (the specs already document which + file/line each locator was confirmed against — follow the same pattern, don't guess). +2. Update the spec in `tests/homolog/ui/`. +3. Re-run `npm run homolog` (or just the affected Playwright spec) against the VPS to + confirm the fix, then commit. + +There is no visual/pixel baseline in this suite (F1) — see Roadmap for that. + +## Roadmap (F2 / F3) + +Design and phased rollout live in the internal planning spec +`_tasks/superpowers/specs/2026-07-13-homolog-e2e-suite-design.md` (not linked — internal +`_tasks/` artifact, not part of this repo's tracked docs). Summary: + +- **F2** — full walkthrough recording → Playwright Test Agents (`planner`/`generator`) + turn it into flow specs (create combo, test provider, edit settings, MCP tools) + + visual regression baseline (Lost Pixel) with masks over dynamic data (metrics, + timestamps, logs) + a `healer` maintenance routine per release. +- **F3** — resilience/contract/wiring coverage: toxiproxy + a fake OpenAI-compatible + provider on the devbox, a `homolog-resilience` combo on the VPS pointed at it + (injected timeout → assert fallback + circuit breaker open/close via + `/api/monitoring/health`); gated Schemathesis contract testing against + `docs/openapi.yaml` (low `--max-examples`, fixed seeds, non-LLM endpoints only); and + wiring `npm run homolog` + its `summary.md` into the `/generate-release` STOP #2 phase. diff --git a/docs/ops/MERGE_TRAIN.md b/docs/ops/MERGE_TRAIN.md new file mode 100644 index 0000000000..d821191b63 --- /dev/null +++ b/docs/ops/MERGE_TRAIN.md @@ -0,0 +1,63 @@ +--- +title: Merge Queue & Manual Merge-Train Runbook +--- + +# Merge Queue & Manual Merge-Train Runbook + +Since v3.8.49 (WS3.2/WS3.4 of the quality/velocity plan) the default merge path for +reviewed PRs into `release/vX.Y.Z` is the **Mergify merge queue** (`.mergify.yml`); +the **manual merge-train** documented below is the FALLBACK — used during incidents, +release freezes, or if the Mergify Open Source plan ever changes. + +## Default path: the Mergify queue + +1. PR is reviewed/greened by the campaigns and approved by the owner's pre-merge ⭐ + gate (the report + per-item decision — see `/merge-prs` Step 0.75). +2. The owner (or the session acting on the owner's decision) applies the **`queue`** + label. The label IS the merge approval; Mergify only executes it. +3. Mergify batches up to 10 queued PRs, validates the batch against the fast-gates, + and merges (squash). A red batch is **bisected automatically** — the offending PR + is isolated in ~log2(N) revalidations and unqueued; the rest proceed. +4. Post-merge, the continuous release-green workflow validates the new tip on push + and opens an attribution issue if the combination regressed (never auto-revert). + +Guardrails (mirror `CLAUDE.md` Hard Rules #21/#22): + +- **Release freeze open** → do NOT label PRs targeting the frozen branch; retarget to + the active `release/vX+1` first. +- **Another session's in-flight PR** → never label it; only the owning session queues + its own work. +- Tests-only diffs and `hotfix`-labeled PRs already run reduced CI (see + `RELEASE_CHECKLIST.md` → Hotfix Fast-Lane); the queue conditions accept whatever + check set actually ran (`#check-failure=0` + `#check-pending=0`). + +## Fallback: the manual merge-train + +Used when the queue is unavailable. This codifies the practice that drained 33 PRs in +one day during the v3.8.47 cycle: + +1. **Assemble the batch** (~10–30 reviewed+approved PRs). Check `linked:` collisions + (same `tap.testFiles`, same CHANGELOG hunks) and serialize those. +2. **Validate ONCE**: in an isolated worktree off the release tip, merge all batch + heads locally, then run the release-equivalent suite + (`npm run check:release-green`, add `--with-build` before a release). +3. **Green** → merge the PRs in sequence (re-checking `state,headRefOid` before each — + a PR whose head moved re-enters review). Prove the net diff of each merge is the + PR's own change (no auto-resolve reverts: audit `git diff --stat` for + out-of-scope deletions). +4. **Red** → bisect the batch by halves (validate each half) instead of re-validating + one-by-one; drop the offending PR back to the review queue with the evidence. +5. **Never**: merge during a freeze into the frozen branch; `git stash` anywhere; + blanket-rerun CI hoping a red goes away (rule: a red is information). + +## Tiering (why the queue is safe with fast-gates only) + +- **Per PR** (quality.yml fast-gates): TIA-impacted tests + full unit 4-shard + + vitest + lint bag + typecheck + docs/changelog integrity. +- **Per batch/tip** (continuous release-green): `--quick` HARD gates on every push to + the release branch; full `--with-build --full-ci` sweeps 3×/day. +- **Per release** (ci.yml on the release PR): the complete matrix incl. E2E ×9, + package-artifact + tarball boot-smoke, coverage/ratchets. + +Nothing is validated less than before — the heavy surface just runs per batch/tip +instead of per PR, which is what removes the O(N) round-trips. diff --git a/docs/ops/RELEASE_CHECKLIST.md b/docs/ops/RELEASE_CHECKLIST.md index 6a0bb0d8bf..4d298c66fd 100644 --- a/docs/ops/RELEASE_CHECKLIST.md +++ b/docs/ops/RELEASE_CHECKLIST.md @@ -37,6 +37,56 @@ npm run test:e2e # optional but recommended /capture-release-evidences-cc ``` +## npm Staged Publishing (default since v3.8.49 — WS1.3/D2) + +The npm-publish workflow no longer publishes directly: it boots the packed tarball +(`check:pack-boot`) and then runs `npm stage publish` — the exact bytes are parked on +the registry, **not installable** until the owner approves. The human 2FA gate moved +to AFTER the proof, not before it. + +**Owner flow after the workflow goes green:** + +1. `npm stage list omniroute` — find the stage id (also printed in the workflow summary). +2. Verify the staged bytes (recommended): `npm stage download `, then install the + downloaded tarball into a temp prefix and boot it (`npm run check:pack-boot` automates + the same pack→install→boot verdict in CI). +3. `npm stage approve ` — the 2FA prompt IS the publish. `npm stage reject ` discards. +4. Post-publish net: the post-publish verifier (WS1.4 of the v3.8.49 plan) installs the + published version from the public registry in a clean container and boots it. + +**Emergency fallback:** `workflow_dispatch` with `publish_mode=direct` restores the +legacy immediate `npm publish` (use only if staging itself misbehaves; record why). + +**One-time hardening (owner, npmjs.com):** configure the Trusted Publisher for +`omniroute` in stage-only mode so a leaked long-lived token cannot `npm publish` +directly from anywhere — CI can only stage; only the owner's 2FA releases. + +**Broken-artifact playbook (unchanged):** `npm deprecate omniroute@ " — use "` +as the default reflex (minutes, reversible); `npm unpublish` only inside the 72h/no-dependents +window and never as the first move. Docker: never rewrite a version tag — rollback is +repointing `latest` to the last good digest. +## Hotfix Fast-Lane (label `hotfix`) + +A PR labeled `hotfix` skips the heavy CI matrix (9-shard E2E, coverage ratchet, +quality-gate, quality-extended) and keeps the fast, high-signal gates: build, +unit shards, integration, vitest, lint/typecheck, docs-sync, `check:pack-artifact` +and the tarball boot-smoke (`check:pack-boot`). Target: green in ≤15min instead of ~33min. + +**Entry policy — all four required (modeled on Chromium/VS Code/Node emergency lanes):** + +1. **Severity**: production is broken — a published artifact crashes on boot / a + security fix / every user of the release is affected. "Important" is not "broken". +2. **Authority**: only the repository owner applies the `hotfix` label. The label IS + the approval — never self-serve on a campaign PR. +3. **Evidence**: the PR body links the previous fully-green heavy run (the suite the + skipped jobs would re-validate) plus the fix's own failing-then-passing test. +4. **Scope**: cherry-pick-only — the minimal fix, no refactors, no ride-alongs. + +The skipped coverage/ratchet surface is re-validated by the next full run on the +release branch (continuous release-green) — the lane skips WAITING, never validation. +Tests-only diffs (all files under `tests/`, none under `tests/e2e/`) skip the E2E +matrix automatically, without any label. + ## Detailed Checklist ### Pre-release diff --git a/docs/ops/RUNNER_BOX.md b/docs/ops/RUNNER_BOX.md new file mode 100644 index 0000000000..07bd70cb04 --- /dev/null +++ b/docs/ops/RUNNER_BOX.md @@ -0,0 +1,35 @@ +--- +title: Self-Hosted Runner Box Operations +--- + +# Self-Hosted Runner Box Operations (.113 pool) + +The self-hosted pool (`self-hosted, omni-release` labels) runs on the 16 GB box at +`192.168.0.113`. Two failure modes recurred on release days and were, until v3.8.49, +manual discipline; the **janitor script codifies them** (WS3.3 of the quality plan): + +1. **Orphaned temp/work dirs** filling the disk → disk-full SQLite errors mid-job. +2. **>4 concurrent runners** → OOM-killed jobs (8-wide killed jobs twice on the + v3.8.47 release day; 4-wide is the proven ceiling). + +## Install the janitor (one-time, on the box) + +```bash +sudo mkdir -p /opt/omniroute-ops +sudo cp scripts/ops/runner-janitor.sh /opt/omniroute-ops/ +sudo chmod +x /opt/omniroute-ops/runner-janitor.sh +( sudo crontab -l 2>/dev/null; echo '*/30 * * * * /opt/omniroute-ops/runner-janitor.sh >> /var/log/runner-janitor.log 2>&1' ) | sudo crontab - +``` + +What it does every 30min: sweeps runner temp leftovers older than 24h, alerts at +≥85% root-disk usage, and alerts when more than the runner ceiling (default 4, tunable +via the script's own environment) of `Runner.Listener` processes are up. Alerts land in `/var/log/runner-janitor.log` +with a non-zero exit (grep for `⚠`). + +## Operating rules + +- **Ceiling: 4 runners** on the 16 GB box. Runners 5–8 stay STOPPED except for + explicit off-peak experiments — never during a release window. +- Stopping a runner mid-job cancels the job (observed live): `systemctl stop` + only when its runner is idle (`Runner.Listener` without a `Runner.Worker` child). +- The `.15` VPS is homologation-only — never runs CI runners. diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index bd8fe06946..7e9b2b0773 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -280,7 +280,7 @@ OmniRoute provides a two-layer defense: request-side injection scanning and resp | `OMNIROUTE_PUBLIC_URL` | _(unset)_ | `open-sse/utils/kieTask.ts` | Public origin used to compose async callback URLs. Lowest-priority fallback for kie.ai callbacks; also used as a generic public URL for other relays. | | `OMNIROUTE_CROF_USAGE_URL` | `https://crof.ai/usage_api/` | `open-sse/services/usage.ts` | CrofAI quota lookup endpoint used by the Usage page. Override for relays / test fixtures. | | `OMNIROUTE_OPENCODE_QUOTA_URL` | `https://opencode.ai/zen/go/v1/quota` | `open-sse/services/opencodeQuotaFetcher.ts` | OpenCode (zen/go) quota lookup endpoint used by the Usage page. Override for relays / test fixtures. | -| `OMNIROUTE_OPENCODE_GO_QUOTA_URL` | `https://api.z.ai/api/monitor/usage/quota/limit` | `open-sse/services/usage.ts` | OpenCode Go quota lookup endpoint used by the Usage page. Override for relays / test fixtures. | +| `OMNIROUTE_OPENCODE_GO_QUOTA_URL` | _(unset)_ | `open-sse/services/opencodeOllamaUsage.ts` | OpenCode Go quota lookup endpoint used by the Usage page. OpenCode Go has no public quota API, so this has no default and the network call is skipped unless the operator opts in to a self-hosted/mirrored endpoint. | | `OMNIROUTE_OPENCODE_GO_DASHBOARD_URL` | `https://opencode.ai/workspace` | `open-sse/services/usage.ts` | OpenCode Go dashboard base URL used for quota scraping when a workspace ID and auth cookie are configured. Override for relays / test fixtures. | | `OPENCODE_GO_WORKSPACE_ID` | _(unset)_ | `open-sse/services/usage.ts` | OpenCode Go workspace ID used for dashboard quota scraping. Prefer the per-connection Dashboard field when multiple accounts are configured. | | `OMNIROUTE_OPENCODE_GO_WORKSPACE_ID` | _(unset)_ | `open-sse/services/usage.ts` | Alternate OpenCode Go workspace ID env var used before the shorter alias. Prefer the per-connection Dashboard field when multiple accounts are configured. | diff --git a/electron/package.json b/electron/package.json index ed52ea15db..b3b5f1a268 100644 --- a/electron/package.json +++ b/electron/package.json @@ -1,6 +1,6 @@ { "name": "omniroute-desktop", - "version": "3.8.48", + "version": "3.8.49", "description": "OmniRoute Desktop Application", "main": "main.js", "author": { diff --git a/next.config.mjs b/next.config.mjs index ac7197503c..b3f7887bb5 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -128,11 +128,25 @@ const nextConfig = { // expected diagnostic — suppress it here rather than fight the analyzer, // mirroring the isNextIntlExtractorDynamicImportWarning precedent below // for the webpack path. (#6582) + // open-sse/services/compression/ruleLoader.ts and + // .../engines/rtk/filterLoader.ts both define an identical + // getModuleDir() helper that walks up directories via + // path.resolve(anchor) + fs.existsSync(...) in a loop with a + // non-literal argument — the same dynamic-path fs access pattern as + // the agentSkills case above, but not covered by that narrower + // allowlist glob, so the "Overly broad patterns..." warning kept + // firing (610 times, once per entry point transitively importing the + // compression module). Same known-benign, bounded fs access; + // suppressed here rather than fought. (#7051, follow-up to #6582) ignoreIssue: [ { path: "**/src/lib/agentSkills/**", description: /Overly broad patterns can lead to build performance issues/, }, + { + path: "**/open-sse/services/compression/**", + description: /Overly broad patterns can lead to build performance issues/, + }, ], }, output: "standalone", diff --git a/open-sse/config/providerErrorRules.ts b/open-sse/config/providerErrorRules.ts index d9eb14535b..dde56a53d2 100644 --- a/open-sse/config/providerErrorRules.ts +++ b/open-sse/config/providerErrorRules.ts @@ -130,6 +130,31 @@ function buildMinimaxRules(): ProviderErrorRule[] { ]; } +// ─── Cloudflare Workers AI ───────────────────────────────────────────────────── +// Free tier = 10,000 Neurons/day, shared across the WHOLE account +// (docs/reference/FREE_TIERS.md; official: developers.cloudflare.com/ +// workers-ai/platform/errors/). The exhaustion body doesn't match any +// QUOTA_PATTERNS keyword so it falls through to rate_limit and gets +// retried every ~60s against a budget that only resets at UTC midnight. +// Issue #6980. +function buildCloudflareAiRules(): ProviderErrorRule[] { + return [ + { + id: "cloudflare-ai-daily-neuron-allocation", + match: ({ status, body }) => { + if (status !== 429) return null; + const text = JSON.stringify(body ?? "").toLowerCase(); + // Body: "you have used up your daily free allocation of 10,000 neurons, + // please upgrade to Cloudflare's Workers Paid plan..." + if (!text.includes("daily free allocation")) return null; + // No cooldownMs: recordModelLockoutFailure already sets + // quota_exhausted without one to "next UTC midnight". + return { reason: "quota_exhausted", scope: "connection" }; + }, + }, + ]; +} + /** * Global registry. Provider name → ordered list of rules (first match wins). * Add new providers here; the matcher in classifyError will pick them up @@ -141,6 +166,7 @@ export const providerRuleRegistry = new Map([ ["opencode-cli", buildOpencodeRules()], ["minimax", buildMinimaxRules()], ["minimax-passthrough", buildMinimaxRules()], + ["cloudflare-ai", buildCloudflareAiRules()], ]); /** @@ -194,7 +220,9 @@ export function getProviderErrorRuleMatch( */ export function parseResetCountdownMs(text: string): number | null { if (typeof text !== "string" || text.length === 0) return null; - const match = text.match(/resets?\s+in\s+(\d+)\s+(day|days|hour|hours|minute|minutes|second|seconds)\b/); + const match = text.match( + /resets?\s+in\s+(\d+)\s+(day|days|hour|hours|minute|minutes|second|seconds)\b/ + ); if (!match) return null; const n = Number(match[1]); if (!Number.isFinite(n) || n <= 0) return null; diff --git a/open-sse/config/providers/registry/grok-cli/index.ts b/open-sse/config/providers/registry/grok-cli/index.ts index 463e103ba6..d6dbbae6ed 100644 --- a/open-sse/config/providers/registry/grok-cli/index.ts +++ b/open-sse/config/providers/registry/grok-cli/index.ts @@ -15,13 +15,25 @@ export const grok_cliProvider: RegistryEntry = { id: "grok-build", name: "Grok Build", contextLength: 256000, - unsupportedParams: ["presencePenalty", "frequencyPenalty", "logprobs", "topLogprobs"], + unsupportedParams: [ + "presencePenalty", + "frequencyPenalty", + "logprobs", + "topLogprobs", + "reasoningEffort", + ], }, { id: "grok-composer-2.5-fast", name: "Grok Composer 2.5 Fast", contextLength: 200000, - unsupportedParams: ["presencePenalty", "frequencyPenalty", "logprobs", "topLogprobs"], + unsupportedParams: [ + "presencePenalty", + "frequencyPenalty", + "logprobs", + "topLogprobs", + "reasoningEffort", + ], }, ], oauth: { diff --git a/open-sse/config/providers/registry/opencode/index.ts b/open-sse/config/providers/registry/opencode/index.ts index 5f3779d72a..4aaa6ea046 100644 --- a/open-sse/config/providers/registry/opencode/index.ts +++ b/open-sse/config/providers/registry/opencode/index.ts @@ -23,29 +23,15 @@ export const opencodeProvider: RegistryEntry = { interleavedField: "reasoning_content", }, { id: "deepseek-v4-flash-free", name: "DeepSeek V4 Flash Free", supportsReasoning: true }, - // #3110: MiniMax M3 free tier via OpenCode - // #3328: MiniMax M3 is multimodal (verified: describes base64 images via the - // opencode upstream) — flag it so vision requests aren't gated/stripped. - { - id: "minimax-m3-free", - name: "MiniMax M3 Free", - contextLength: 1048576, - supportsVision: true, - }, - { id: "minimax-m2.5-free", name: "MiniMax M2.5 Free", contextLength: 204800 }, - { id: "ling-2.6-1t-free", name: "Ling 2.6 Free", contextLength: 262000 }, - { - id: "trinity-large-preview-free", - name: "Trinity Large Preview Free", - contextLength: 131000, - }, - { id: "nemotron-3-super-free", name: "Nemotron 3 Super Free", contextLength: 1000000 }, - { - id: "qwen3.6-plus-free", - name: "Qwen3.6 Plus Free", - targetFormat: "claude", - supportsVision: false, - contextLength: 200000, - }, + // #6998: 2026-07-14 refresh — the upstream free tier rotated its lineup; + // minimax-m3-free, minimax-m2.5-free, ling-2.6-1t-free, + // trinity-large-preview-free, nemotron-3-super-free and qwen3.6-plus-free + // were delisted (401 "Model X is not supported") and replaced by the 4 + // entries below, confirmed live against + // https://opencode.ai/zen/v1/chat/completions. + { id: "mimo-v2.5-free", name: "MiMo V2.5 Free", contextLength: 131000 }, + { id: "hy3-free", name: "HY3 Free", contextLength: 131000 }, + { id: "nemotron-3-ultra-free", name: "Nemotron 3 Ultra Free", contextLength: 1000000 }, + { id: "north-mini-code-free", name: "North Mini Code Free", contextLength: 131000 }, ], }; diff --git a/open-sse/executors/antigravity.ts b/open-sse/executors/antigravity.ts index 494d750836..49b1d8ff37 100644 --- a/open-sse/executors/antigravity.ts +++ b/open-sse/executors/antigravity.ts @@ -1591,6 +1591,34 @@ export class AntigravityExecutor extends BaseExecutor { }; } + // #2461: a non-ok upstream response (e.g. 403) must never be piped through the + // streaming pass-through below as if it were an SSE body. Google occasionally + // returns non-UTF8/binary error bodies (observed: gzip-magic-byte payloads) for + // 403s on this endpoint; reading/forwarding those raw bytes corrupts the + // client-visible error message. Mirror the non-streaming branch above and build + // a sanitized JSON error via buildAntigravityUpstreamError (hard rule #12) + // instead of streaming unknown bytes straight through. + if (!response.ok) { + const rawBody = await response + .clone() + .text() + .catch(() => ""); + const errorBody = buildAntigravityUpstreamError( + response.status, + response.statusText, + rawBody + ); + return { + response: new Response(JSON.stringify(errorBody), { + status: response.status, + headers: { "Content-Type": "application/json" }, + }), + url, + headers: finalHeaders, + transformedBody: attachToolNameMap(transformedBody, requestToolNameMap), + }; + } + // Streaming path: wrap the response body in a pass-through TransformStream // that extracts remainingCredits from the final SSE chunk(s) without // consuming the stream. The client receives the unmodified SSE data. diff --git a/open-sse/executors/codex/tools.ts b/open-sse/executors/codex/tools.ts index 3337000359..52d01e9d87 100644 --- a/open-sse/executors/codex/tools.ts +++ b/open-sse/executors/codex/tools.ts @@ -1,6 +1,8 @@ // Codex Responses-API tool normalization (hosted-tool passthrough + free-plan gating). // Extracted verbatim from codex.ts. Self-contained (console.debug only). +import { stripUnsupportedRegexPatterns } from "../../translator/helpers/schemaCoercion.ts"; + // Responses-API hosted tool types that OpenAI/Codex executes server-side. // These arrive shaped as `{ type, ...params }` with no `function` object and no `name` — // e.g. Codex CLI injects `{ type: "image_generation", output_format: "png" }` or @@ -133,6 +135,11 @@ export function normalizeCodexTools( ? functionObject.strict : undefined; + // Codex/OpenAI Responses API rejects `pattern` fields using regex lookaround + // (e.g. `^(?=.*@).+$`) with a 400 "regex lookaround is not supported" error. + // Strip those before the schema reaches upstream (9router#1556). + const sanitizedParameters = stripUnsupportedRegexPatterns(parameters); + // Rewrite in-place to Responses format for (const key of Object.keys(tool)) { delete tool[key]; @@ -140,7 +147,7 @@ export function normalizeCodexTools( tool.type = "function"; tool.name = name.slice(0, 128); if (description) tool.description = description; - tool.parameters = parameters; + tool.parameters = sanitizedParameters; if (strict !== undefined) tool.strict = strict; validToolNames.add(name); diff --git a/open-sse/executors/duckduckgo-web.ts b/open-sse/executors/duckduckgo-web.ts index e9c6053548..bf653932c7 100644 --- a/open-sse/executors/duckduckgo-web.ts +++ b/open-sse/executors/duckduckgo-web.ts @@ -64,11 +64,18 @@ function shouldUseBrowserBacked(): boolean { interface DuckDuckGoVqdHeaders { vqd4: string | null; vqdHash1: string | null; + // #6996: the real upstream HTTP status of the VQD-acquisition attempt (null when + // no request was made / a network error was thrown). Lets execute() distinguish a + // retryable 429 rate-limit from a genuine 5xx instead of collapsing both to 503. + status: number | null; + retryAfter: string | null; } interface DuckDuckGoAuthHeaders { vqd4: string | null; vqdHash1: string | null; + status: number | null; + retryAfter: string | null; } interface DuckDuckGoModelCapabilities { @@ -369,10 +376,13 @@ export class DuckDuckGoWebExecutor extends BaseExecutor { const isStreaming = stream !== false; const upstreamHeaders = upstreamExtraHeaders || {}; - const errorResponse = (status: number, message: string): Response => + const errorResponse = (status: number, message: string, retryAfter?: string | null): Response => new Response(JSON.stringify({ error: { message } }), { status, - headers: { "Content-Type": "application/json" }, + headers: { + "Content-Type": "application/json", + ...(retryAfter ? { "Retry-After": retryAfter } : {}), + }, }); if (messages.length === 0) { @@ -468,6 +478,19 @@ export class DuckDuckGoWebExecutor extends BaseExecutor { const vqdHeaders = await this.acquireAuthHeaders(mergedSignal); if (!vqdHeaders.vqd4 && !vqdHeaders.vqdHash1) { clearTimeout(timeout); + // #6996: surface the real upstream status instead of a hardcoded 503 so a + // 429 rate-limit gets a connection-cooldown, not a whole-provider circuit + // breaker trip (see CLAUDE.md "Provider Circuit Breaker" — only + // 408/500/502/503/504 should trip it, not 429). Any other non-2xx status + // (403 anti-bot challenge, genuine 5xx, or a thrown network error where + // status is null) keeps the existing 503 fallback. + if (vqdHeaders.status === 429) { + return errorResponse( + 429, + "Failed to acquire VQD token: upstream rate limited", + vqdHeaders.retryAfter + ); + } return errorResponse(503, "Failed to acquire VQD token"); } @@ -555,16 +578,25 @@ export class DuckDuckGoWebExecutor extends BaseExecutor { }); this.rememberResponseCookies(resp); - if (!resp.ok) return { vqd4: null, vqdHash1: null }; + if (!resp.ok) { + return { + vqd4: null, + vqdHash1: null, + status: resp.status, + retryAfter: resp.headers.get("Retry-After"), + }; + } return { vqd4: resp.headers.get("x-vqd-4"), vqdHash1: resp.headers.get("x-vqd-hash-1"), + status: resp.status, + retryAfter: null, }; } catch (error) { if (error instanceof DOMException && error.name === "AbortError") { throw error; } - return { vqd4: null, vqdHash1: null }; + return { vqd4: null, vqdHash1: null, status: null, retryAfter: null }; } } @@ -576,6 +608,8 @@ export class DuckDuckGoWebExecutor extends BaseExecutor { return { vqd4: null, vqdHash1: await solveDuckDuckGoChallenge(challenge, FAKE_HEADERS["User-Agent"]), + status: null, + retryAfter: null, }; } catch (error) { void error; @@ -588,6 +622,8 @@ export class DuckDuckGoWebExecutor extends BaseExecutor { return { vqd4: headers.vqd4, vqdHash1: await solveDuckDuckGoChallenge(headers.vqdHash1, FAKE_HEADERS["User-Agent"]), + status: headers.status, + retryAfter: headers.retryAfter, }; } catch (error) { void error; diff --git a/open-sse/executors/gemini-web.ts b/open-sse/executors/gemini-web.ts index 48a4cbbfdb..0286cc8202 100644 --- a/open-sse/executors/gemini-web.ts +++ b/open-sse/executors/gemini-web.ts @@ -112,12 +112,15 @@ function parseCookies(raw: string): Array<{ name: string; value: string }> { * [["wrb.fr", null, ""]] * * The JSON string contains nested array: inner[4][0][1] = ["text chunks"]. - * We concatenate text from every wrb.fr line because Gemini can split one - * assistant answer across multiple StreamGenerate chunks. + * Each wrb.fr line is a CUMULATIVE snapshot of the whole answer generated so + * far (not an independent delta), so we keep only the text from the LAST + * frame that yields non-empty text instead of concatenating every frame — + * concatenating would reproduce the same growing text with each snapshot + * (see #7163). */ export function parseStreamResponse(raw: string): string { const lines = raw.split("\n"); - const textChunks: string[] = []; + let lastText = ""; for (const rawLine of lines) { const line = rawLine.trim(); @@ -133,12 +136,12 @@ export function parseStreamResponse(raw: string): string { const responseArray = inner?.[4]?.[0]?.[1]; if (!Array.isArray(responseArray)) continue; const text = responseArray.filter((c: unknown) => typeof c === "string").join(""); - if (text) textChunks.push(text); + if (text) lastText = text; } catch { // Skip unparseable lines } } - return textChunks.join(""); + return lastText; } function readCredentialString(value: unknown): string { diff --git a/open-sse/executors/index.ts b/open-sse/executors/index.ts index 039e7fa637..68168fd8cf 100644 --- a/open-sse/executors/index.ts +++ b/open-sse/executors/index.ts @@ -170,8 +170,26 @@ const executors = { const defaultCache = new Map(); +// #6699 — providers that exist ONLY as Cloud Agent task-API entries +// (CLOUD_AGENT_PROVIDERS / staticModels "Available Models" catalog) and have no +// chat-completions REGISTRY entry anywhere in open-sse/. Without this guard, +// getExecutor() silently falls through to DefaultExecutor's +// `PROVIDERS[provider] || PROVIDERS.openai` fallback, sending the user's real +// provider key to OpenAI's endpoint (mislabeled as coming from the provider the +// user actually selected). Starting with just "jules" (the reported case); +// "devin" and "codex-cloud" share the same structural gap and are left for a +// follow-up once their own chat-routing behavior is confirmed. +const CHAT_UNSUPPORTED_CLOUD_AGENT_PROVIDERS = new Set(["jules"]); + export function getExecutor(provider) { if (executors[provider]) return executors[provider]; + if (CHAT_UNSUPPORTED_CLOUD_AGENT_PROVIDERS.has(provider)) { + const err = new Error( + `Provider "${provider}" is a cloud-agent provider and does not support direct chat completions; use the Cloud Agents task API instead.` + ); + (err as Error & { status?: number }).status = 400; + throw err; + } if (!defaultCache.has(provider)) defaultCache.set(provider, new DefaultExecutor(provider)); return defaultCache.get(provider); } diff --git a/open-sse/executors/mimocode.ts b/open-sse/executors/mimocode.ts index 1313a1d2ab..115e5a4bad 100644 --- a/open-sse/executors/mimocode.ts +++ b/open-sse/executors/mimocode.ts @@ -90,6 +90,7 @@ export interface AccountProxyConfig { port: number; username?: string; password?: string; + relayAuth?: string; } | null; } diff --git a/open-sse/executors/opencode.ts b/open-sse/executors/opencode.ts index 60fe0f7951..2453199b8f 100644 --- a/open-sse/executors/opencode.ts +++ b/open-sse/executors/opencode.ts @@ -21,6 +21,7 @@ export interface OpencodeAccountProxyConfig { port: number; username?: string; password?: string; + relayAuth?: string; } | null; } diff --git a/open-sse/executors/yuanbao-web.ts b/open-sse/executors/yuanbao-web.ts index 20ef5662e0..75a3d2c615 100644 --- a/open-sse/executors/yuanbao-web.ts +++ b/open-sse/executors/yuanbao-web.ts @@ -106,8 +106,13 @@ function buildPrompt(messages: Array>): string { /** Build the `hy_source=web; hy_user=...; hy_token=...` cookie from the pasted header. */ function buildYuanbaoCookie(rawApiKey: string): { cookie: string; hasToken: boolean } { const raw = stripCookieInputPrefix(rawApiKey || ""); - const hyUser = extractCookieValue(raw, "hy_user"); - const hyToken = extractCookieValue(raw, "hy_token"); + // Guard the extractCookieValue bare-value fallback: for input that is a single + // FOREIGN pair (e.g. "some_other=abc") the helper returns the whole string, which + // used to fool this validation into forwarding garbage upstream (the request only + // failed when Tencent replied 401 — a live-network dependency). Yuanbao needs the + // two distinct cookies, so only trust an extraction the input explicitly names. + const hyUser = raw.includes("hy_user=") ? extractCookieValue(raw, "hy_user") : null; + const hyToken = raw.includes("hy_token=") ? extractCookieValue(raw, "hy_token") : null; if (hyUser && hyToken) { return { cookie: `hy_source=web; hy_user=${hyUser}; hy_token=${hyToken}`, hasToken: true }; diff --git a/open-sse/package.json b/open-sse/package.json index fd014cab85..c53d7d28fd 100644 --- a/open-sse/package.json +++ b/open-sse/package.json @@ -1,6 +1,6 @@ { "name": "@omniroute/open-sse", - "version": "3.8.48", + "version": "3.8.49", "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 275ecbab41..32562fc526 100644 --- a/open-sse/services/accountFallback.ts +++ b/open-sse/services/accountFallback.ts @@ -41,6 +41,7 @@ import { isSubscriptionQuotaText, buildSubscriptionQuotaFallback, buildWeeklyQuotaFallback, + buildSessionQuotaFallback, } from "./quotaTextCooldowns.ts"; import { parseDayGranularityResetMs, shouldPreserveQuotaSignals } from "./quotaResetParsing.ts"; @@ -1454,6 +1455,12 @@ export function checkFallbackError( } const weeklyResult = buildWeeklyQuotaFallback(errorStr); if (weeklyResult) return weeklyResult; + // Issue #7071 (session usage cap) is the same sibling gap as #3709 above — + // runs UNCONDITIONALLY for the same reason: apikey-category providers + // like ollama-cloud are excluded from the oauth-only shouldUseQuotaSignal + // gate. + const sessionResult = buildSessionQuotaFallback(errorStr); + if (sessionResult) return sessionResult; const quotaResetHintMs = parseRetryFromErrorText(errorStr); if ( diff --git a/open-sse/services/autoCombo/__tests__/speedRanking.test.ts b/open-sse/services/autoCombo/__tests__/speedRanking.test.ts index 1599e36b82..e3a4ad85f2 100644 --- a/open-sse/services/autoCombo/__tests__/speedRanking.test.ts +++ b/open-sse/services/autoCombo/__tests__/speedRanking.test.ts @@ -165,15 +165,34 @@ describe("rankBySpeed — factor breakdown", () => { }); it("falls back to 0.5 per missing metric so new providers are not crushed", () => { - const ranked = rankBySpeed([candidate({ provider: "fresh", model: "m" })]); + const ranked = rankBySpeed([ + candidate({ + provider: "fresh", + model: "m", + p95LatencyMs: undefined, + latencyStdDev: undefined, + }), + ]); expect(ranked).toHaveLength(1); // No telemetry at all → weighted sum lands near 0.5 with reliability multiplier 1 expect(ranked[0].factors.reliability).toBe(1); expect(ranked[0].factors.health).toBe(1); expect(ranked[0].factors.ttft).toBe(0.5); - expect(ranked[0].factors.tps).toBe(0.5); - }); -}); + expect(ranked[0].factors.tps).toBe(0.5); + }); + + it("uses p95 latency when TTFT and E2E telemetry are unavailable", () => { + const ranked = rankBySpeed([ + candidate({ provider: "slow-tail", model: "m", p95LatencyMs: 4000 }), + candidate({ provider: "fast-tail", model: "m", p95LatencyMs: 1000 }), + ]); + const fast = ranked.find((entry) => entry.provider === "fast-tail"); + const slow = ranked.find((entry) => entry.provider === "slow-tail"); + + expect(fast?.factors.ttft).toBeGreaterThan(slow?.factors.ttft ?? 1); + expect(fast?.factors.e2e).toBeGreaterThan(slow?.factors.e2e ?? 1); + }); +}); describe("rankBySpeed — weight overrides", () => { it("respects caller weight overrides (e.g. heavy TTFT bias)", () => { @@ -223,4 +242,4 @@ describe("pickFastest", () => { const winner = pickFastest([slow, fast]); expect(winner?.provider).toBe("fast"); }); -}); \ No newline at end of file +}); diff --git a/open-sse/services/autoCombo/speedRanking.ts b/open-sse/services/autoCombo/speedRanking.ts index 514420a5a0..a3d7d81027 100644 --- a/open-sse/services/autoCombo/speedRanking.ts +++ b/open-sse/services/autoCombo/speedRanking.ts @@ -211,9 +211,15 @@ function speedFactorsFor( failureRate: number ): SpeedFactors { return { - ttft: lowerIsBetter(positiveFinite(candidate.avgTtftMs), maxima.ttft), + ttft: lowerIsBetter( + positiveFinite(candidate.avgTtftMs) ?? positiveFinite(candidate.p95LatencyMs), + maxima.ttft + ), tps: higherIsBetter(positiveFinite(candidate.avgTokensPerSecond), maxima.tps), - e2e: lowerIsBetter(positiveFinite(candidate.avgE2ELatencyMs), maxima.e2e), + e2e: lowerIsBetter( + positiveFinite(candidate.avgE2ELatencyMs) ?? positiveFinite(candidate.p95LatencyMs), + maxima.e2e + ), p95: lowerIsBetter(positiveFinite(candidate.p95LatencyMs), maxima.p95), health: healthScoreFor(candidate.circuitBreakerState), reliability: clamp01(1 - failureRate), diff --git a/open-sse/services/claudeTlsClient.ts b/open-sse/services/claudeTlsClient.ts index 499c32e130..0e785af9f5 100644 --- a/open-sse/services/claudeTlsClient.ts +++ b/open-sse/services/claudeTlsClient.ts @@ -344,7 +344,19 @@ function toHeaders(raw: Record): Headers { // to a file path, terminating when the upstream sends `streamOutputEOFSymbol`. // We tail the file from a worker and surface the bytes as a ReadableStream. -async function tlsFetchStreaming( +// Cap for the bounded fallback read of a non-SSE error body straight from the +// streaming temp file (mirrors the 2048-byte cap executors/claude-web.ts +// already applies when reading error bodies) — avoids buffering an unbounded +// error page into memory. See #7134. +const MAX_ERROR_BODY_BYTES = 16 * 1024; + +/** + * Exported for tests (issue #7134): allows injecting a fake `client` so the + * non-SSE error-body fallback path can be exercised without + * `--experimental-test-module-mocks`, matching the DI pattern already used + * by `__setTlsFetchOverrideForTesting` for the outer `tlsFetchClaude`. + */ +export async function tlsFetchStreaming( client: { request: (url: string, opts: Record) => Promise }, url: string, requestOptions: Record, @@ -417,11 +429,22 @@ async function tlsFetchStreaming( const r = await requestPromise.catch( (e) => ({ status: 502, headers: {}, body: String(e) }) as TlsResponseLike ); + // tls-client-node's `streamOutputPath` mode writes the response body to + // the temp file chunk-by-chunk and does NOT also populate the resolved + // response's in-memory `body` field (confirmed against + // node_modules/tls-client-node/dist/response.js) — so for every non-SSE, + // non-2xx claude-web response (400/403/429/500 with a real JSON/HTML + // error), `r.body` is empty even though the real bytes are sitting in + // `path` (we just peeked them above). Prefer `r.body` when it IS + // populated (some native-client modes do fill it in); otherwise fall + // back to a bounded read of the temp file so the real upstream error + // detail reaches the caller instead of being silently discarded. #7134 + const text = r.body || (await readFirstBytes(path, MAX_ERROR_BODY_BYTES).catch(() => "")); await cleanupTempPath(path); return { status: r.status, headers: toHeaders(r.headers), - text: r.body, + text, body: null, }; } diff --git a/open-sse/services/combo/validateQuality.ts b/open-sse/services/combo/validateQuality.ts index 2b56146479..7f1a32b38a 100644 --- a/open-sse/services/combo/validateQuality.ts +++ b/open-sse/services/combo/validateQuality.ts @@ -54,6 +54,91 @@ function extractEnvelopeErrorText(json: Record): string | null return parts.length > 0 ? parts.join(" ") : null; } +/** Mutable lifecycle flags threaded through {@link applySseLifecycleEvent}. */ +interface SseLifecycleFlags { + hasMessageStart: boolean; + hasContentBlock: boolean; + hasRealContent: boolean; + hasLifecycleEnd: boolean; +} + +/** Read `parsed.` as a nested object bag, or null when absent/not an object. */ +function asObject(parsed: Record, key: string): Record | null { + const value = parsed[key]; + return value && typeof value === "object" ? (value as Record) : null; +} + +/** + * A content_block_start is real signal only for tool_use / redacted_thinking — + * a tool call is meaningful even before its input_json_delta arrives. text and + * thinking blocks routinely open empty; keep peeking for a delta instead. + */ +function contentBlockStartIsRealSignal(parsed: Record): boolean { + const blockType = asObject(parsed, "content_block")?.type; + return blockType === "tool_use" || blockType === "redacted_thinking"; +} + +/** + * A content_block_delta is real signal when it carries non-empty text/thinking, + * or any input_json_delta fragment — even an empty-string first chunk proves a + * tool_use block is actively streaming its arguments. + */ +function contentBlockDeltaIsRealSignal(parsed: Record): boolean { + const delta = asObject(parsed, "delta"); + if (!delta) return false; + const deltaType = typeof delta.type === "string" ? delta.type : ""; + if (deltaType === "input_json_delta") return true; + if (deltaType !== "text_delta" && deltaType !== "thinking_delta") return false; + const text = delta.text ?? delta.thinking; + return typeof text === "string" && text.length > 0; +} + +/** A message_delta closes the lifecycle once it carries a stop_reason. */ +function messageDeltaEndsLifecycle(parsed: Record): boolean { + return asObject(parsed, "delta")?.stop_reason != null; +} + +/** + * Apply a single parsed Claude SSE event to the peeked lifecycle `flags` + * (mutated in place). Extracted from `parseAccumulatedSse`'s inline switch to + * keep that function under the complexity/line ratchets — logic unchanged. + * + * Returns true once REAL content (not just an empty content_block_start) is + * detected — the caller should stop peeking and treat the stream as non-empty. + */ +function applySseLifecycleEvent( + eventType: string, + parsed: Record, + flags: SseLifecycleFlags +): boolean { + switch (eventType) { + case "message_start": + flags.hasMessageStart = true; + return false; + case "content_block_start": + flags.hasContentBlock = true; + if (!contentBlockStartIsRealSignal(parsed)) return false; + flags.hasRealContent = true; + return true; + case "content_block_delta": + flags.hasContentBlock = true; + if (!contentBlockDeltaIsRealSignal(parsed)) return false; + flags.hasRealContent = true; + return true; + case "content_block_stop": + flags.hasContentBlock = true; + return false; + case "message_stop": + flags.hasLifecycleEnd = true; + return false; + case "message_delta": + if (messageDeltaEndsLifecycle(parsed)) flags.hasLifecycleEnd = true; + return false; + default: + return false; + } +} + function responsesApiOutputHasContent(output: unknown): boolean { return ( Array.isArray(output) && @@ -125,9 +210,22 @@ export async function validateResponseQuality( let decodedSoFar = ""; // SSE lifecycle state. - let hasMessageStart = false; - let hasContentBlock = false; - let hasLifecycleEnd = false; + // + // #1382: hasContentBlock only means "a content_block_* event was observed" + // — it does NOT mean the block carried usable content. A content_block_start + // for a text/thinking block routinely opens with empty text (real content + // arrives via subsequent content_block_delta events); some upstreams + // (reported: DeepSeek/GLM via claude→openai translation on tool-heavy + // requests) open and close such a block without ever emitting a delta. + // hasRealContent tracks whether we've actually seen usable output: a + // tool_use/redacted_thinking block start (self-evidently real, even before + // any delta), or a delta carrying non-empty text/thinking/tool-input. + const sse: SseLifecycleFlags = { + hasMessageStart: false, + hasContentBlock: false, + hasRealContent: false, + hasLifecycleEnd: false, + }; let anyContentFound = false; let sawAnyBytes = false; const sseLineNormalizer = createSSEDataLineNormalizer(); @@ -138,8 +236,9 @@ export async function validateResponseQuality( * flags in the closure. The last (potentially incomplete) line is kept in * `decodedSoFar` for the next iteration. * - * Returns true when a content_block_* event is detected — the caller - * should stop peeking and treat the stream as non-empty. + * Returns true once REAL content (not just an empty content_block_start) + * is detected — the caller should stop peeking and treat the stream as + * non-empty. */ function parseAccumulatedSse(): boolean { const lines = decodedSoFar.split(/\r?\n/); @@ -177,32 +276,8 @@ export async function validateResponseQuality( return true; } - switch (eventType) { - case "message_start": - hasMessageStart = true; - break; - case "content_block_start": - case "content_block_delta": - case "content_block_stop": - hasContentBlock = true; - // Signal caller to stop buffering immediately. - return true; - case "message_stop": - hasLifecycleEnd = true; - break; - case "message_delta": { - const delta = parsed.delta; - if ( - delta && - typeof delta === "object" && - (delta as Record).stop_reason != null - ) { - hasLifecycleEnd = true; - } - break; - } - default: - break; + if (applySseLifecycleEvent(eventType, parsed, sse)) { + return true; } } return false; @@ -258,11 +333,17 @@ export async function validateResponseQuality( if (decodedSoFar.trim()) decodedSoFar += "\n\n"; parseAccumulatedSse(); - if (hasMessageStart && hasLifecycleEnd && !hasContentBlock) { - // Complete Claude lifecycle with zero content blocks → failover. + if (sse.hasMessageStart && sse.hasLifecycleEnd && !sse.hasRealContent) { + // Complete Claude lifecycle with zero content blocks, or with + // content_block_start/stop pairs that never carried real text/ + // thinking/tool_use content (#1382 — tool-heavy claude→openai + // requests against upstreams like DeepSeek/GLM can "complete" a + // lifecycle around an empty block) → failover. log.warn?.( "COMBO", - "Streaming Claude response has complete lifecycle but zero content blocks (content_filter?) — marking as invalid for combo failover" + sse.hasContentBlock + ? "Streaming Claude response has complete lifecycle but its content block(s) carried no usable text/tool_use — marking as invalid for combo failover" + : "Streaming Claude response has complete lifecycle but zero content blocks (content_filter?) — marking as invalid for combo failover" ); return { valid: false, reason: "streaming empty content block" }; } @@ -273,7 +354,7 @@ export async function validateResponseQuality( // (an explicit `data: [DONE]`, ping/metadata events, an incomplete // Claude lifecycle) keep the pass-through contract (#3399/#3685): // those are handled by the stream-readiness timeout, not failover. - if (!anyContentFound && !hasContentBlock && !sawAnyBytes) { + if (!anyContentFound && !sse.hasContentBlock && !sawAnyBytes) { log.warn?.( "COMBO", "Streaming response ended with no recognized content — marking as invalid for combo failover" diff --git a/open-sse/services/compression/engines/headroom/smartcrusher.ts b/open-sse/services/compression/engines/headroom/smartcrusher.ts index c40ea82928..a14c851b6b 100644 --- a/open-sse/services/compression/engines/headroom/smartcrusher.ts +++ b/open-sse/services/compression/engines/headroom/smartcrusher.ts @@ -160,7 +160,7 @@ export function collectCompactableArrays( while ((m = regex.exec(text)) !== null) pushIfCompactable(m[1].trim()); }; for (const msg of messages) { - if (msg.role === "system") continue; + if (msg.role === "system" || msg.role === "developer") continue; if (typeof msg.content === "string") scanText(msg.content); else if (Array.isArray(msg.content)) { for (const part of msg.content) { @@ -218,8 +218,12 @@ export function crushMessages( let changed = false; const result = messages.map((msg): MessageLike => { - // Guard: never touch system messages - if (msg.role === "system") return { ...msg }; + // Guard: never touch system messages. "developer" is the Responses-API equivalent of + // "system" used by newer models (e.g. Codex CLI, see open-sse/executors/codex.ts) and + // carries the same kind of instructions/tool-schema content — compacting a JSON array + // embedded there (e.g. an update_plan example) can corrupt the model's tool-calling + // instructions (9router#2132: broke Codex CLI plan mode). + if (msg.role === "system" || msg.role === "developer") return { ...msg }; if (typeof msg.content === "string") { const crushed = crushText(msg.content, minRows); diff --git a/open-sse/services/fusion.ts b/open-sse/services/fusion.ts index 000f5a21c5..d6e5f4fa1f 100644 --- a/open-sse/services/fusion.ts +++ b/open-sse/services/fusion.ts @@ -27,12 +27,20 @@ export const FUSION_DEFAULTS = { minPanel: 2, // answers needed before stragglers get a grace window stragglerGraceMs: 8000, // wait this long for laggards once quorum is reached panelHardTimeoutMs: 90000, // absolute cap so one hung model can't stall forever + // Hard cap on panel size (issue #1905). Every panel member is fanned out in + // parallel and its full response text buffered in memory simultaneously — + // with the runtime heap capped (Dockerfile OMNIROUTE_MEMORY_MB, default + // 1024MB), a large panel (reported: ~73 models) with sizable concurrent + // responses can exceed the heap ceiling and OOM-crash the whole process. + // Reject oversized panels up front with a clean 400 instead. + maxPanel: 40, } as const; export type FusionTuning = { minPanel?: number; stragglerGraceMs?: number; panelHardTimeoutMs?: number; + maxPanel?: number; }; type Body = Record; @@ -246,6 +254,21 @@ export async function handleFusionChat({ return handleSingleModel(body, panel[0]); } + // Reject an oversized panel BEFORE fan-out (issue #1905): fanning out N + // parallel calls and buffering N full response bodies at once is what + // drives the process into an OOM crash, not any one call in isolation. + const maxPanel = tuning?.maxPanel ?? FUSION_DEFAULTS.maxPanel; + if (panel.length > maxPanel) { + log.warn( + "FUSION", + `Combo "${comboName ?? ""}" panel=${panel.length} exceeds maxPanel=${maxPanel} — rejecting before fan-out (#1905)` + ); + return errorResponse( + 400, + `Fusion panel too large (${panel.length} models, max ${maxPanel}) — reduce the combo's target count or raise fusionTuning.maxPanel` + ); + } + const cfg = { minPanel: tuning?.minPanel ?? FUSION_DEFAULTS.minPanel, stragglerGraceMs: tuning?.stragglerGraceMs ?? FUSION_DEFAULTS.stragglerGraceMs, diff --git a/open-sse/services/opencodeOllamaUsage.ts b/open-sse/services/opencodeOllamaUsage.ts index 25cd3c3cc9..b3beb4606d 100644 --- a/open-sse/services/opencodeOllamaUsage.ts +++ b/open-sse/services/opencodeOllamaUsage.ts @@ -13,8 +13,13 @@ type UsageQuota = { currency?: string; }; -const OPENCODE_GO_QUOTA_URL = - process.env.OMNIROUTE_OPENCODE_GO_QUOTA_URL ?? "https://api.z.ai/api/monitor/usage/quota/limit"; +// OpenCode Go does not expose a public quota API. There is no working +// opencode.ai endpoint to default to (see #7022) — the quota-by-API-key path +// below is opt-in only and activates exclusively when the operator sets +// OMNIROUTE_OPENCODE_GO_QUOTA_URL explicitly. Never hardcode a third-party +// host here (a previous default silently sent the user's API key to an +// unrelated Z.AI endpoint). +const OPENCODE_GO_QUOTA_URL = process.env.OMNIROUTE_OPENCODE_GO_QUOTA_URL?.trim() || ""; const OPENCODE_GO_DASHBOARD_BASE_URL = process.env.OMNIROUTE_OPENCODE_GO_DASHBOARD_URL ?? "https://opencode.ai/workspace"; const OPENCODE_GO_QUOTA_TOTALS = { session: 12, weekly: 30, mcp_monthly: 60 } as const; @@ -335,6 +340,15 @@ export async function getOpenCodeGoUsage(apiKey: string, providerSpecificData?: }; } + if (!OPENCODE_GO_QUOTA_URL) { + return { + message: + "OpenCode Go does not expose a public quota API. " + + "Set OPENCODE_GO_WORKSPACE_ID and OPENCODE_GO_AUTH_COOKIE to enable dashboard quota scraping, " + + "or set OMNIROUTE_OPENCODE_GO_QUOTA_URL to opt in to an explicit quota endpoint.", + }; + } + try { const res = await fetch(OPENCODE_GO_QUOTA_URL, { headers: { @@ -348,7 +362,8 @@ export async function getOpenCodeGoUsage(apiKey: string, providerSpecificData?: if (res.status === 401 || res.status === 403) { return { message: - "OpenCode Go API key is valid for chat/models but cannot read quota from the Z.AI quota API. " + + "OpenCode Go API key is valid for chat/models but cannot read quota from the configured " + + "OMNIROUTE_OPENCODE_GO_QUOTA_URL endpoint. " + "Set OPENCODE_GO_WORKSPACE_ID and OPENCODE_GO_AUTH_COOKIE to enable dashboard quota scraping.", }; } @@ -374,7 +389,8 @@ export async function getOpenCodeGoUsage(apiKey: string, providerSpecificData?: ) { return { message: - "OpenCode Go API key is valid for chat/models but cannot read quota from the Z.AI quota API. " + + "OpenCode Go API key is valid for chat/models but cannot read quota from the configured " + + "OMNIROUTE_OPENCODE_GO_QUOTA_URL endpoint. " + "Set OPENCODE_GO_WORKSPACE_ID and OPENCODE_GO_AUTH_COOKIE to enable dashboard quota scraping.", }; } diff --git a/open-sse/services/quotaTextCooldowns.ts b/open-sse/services/quotaTextCooldowns.ts index 0146d72ea5..4f481b822d 100644 --- a/open-sse/services/quotaTextCooldowns.ts +++ b/open-sse/services/quotaTextCooldowns.ts @@ -103,3 +103,37 @@ export function buildWeeklyQuotaFallback(errorStr: string): QuotaTextFallback | reason: RateLimitReason.QUOTA_EXHAUSTED, }; } + +// ─── Issue #7071 — Ollama Cloud 5-hour SESSION usage cap ─────────────────── +// +// Ollama Cloud also enforces a rolling 5-hour "session" usage cap, sibling to +// the weekly cap above (#3709/#6638). On cap the upstream returns 429 with a +// body like "you () have reached your session usage limit". Same +// root cause as the weekly gap: neither the generic subscription-quota-text +// classifier nor the weekly one recognize "session" wording, so the account +// fell through to the generic 429 backoff and got retried within the same +// 5-hour window instead of cooling down for it — combo/LKGP routing cycled +// back to the "exhausted" account instead of advancing to the next one. +// +// Patterns are scoped to "session ... usage limit" / "session limit reached" +// / "reached your session ... usage limit" phrasing (not a bare "session" +// match) so unrelated "session expired"/"session token invalid" auth errors +// from other providers are not misclassified as quota-exhausted. +const SESSION_QUOTA_COOLDOWN_MS = 5 * 60 * 60 * 1000; // 5 hours + +export function isSessionUsageLimitText(lower: string): boolean { + return ( + lower.includes("session usage limit") || + lower.includes("session limit reached") || + (lower.includes("reached your session") && lower.includes("usage limit")) + ); +} + +export function buildSessionQuotaFallback(errorStr: string): QuotaTextFallback | null { + if (!isSessionUsageLimitText(errorStr.toLowerCase())) return null; + return { + shouldFallback: true, + cooldownMs: SESSION_QUOTA_COOLDOWN_MS, + reason: RateLimitReason.QUOTA_EXHAUSTED, + }; +} diff --git a/open-sse/services/usage.ts b/open-sse/services/usage.ts index 39f8296082..1083748e9b 100644 --- a/open-sse/services/usage.ts +++ b/open-sse/services/usage.ts @@ -525,6 +525,7 @@ export const USAGE_FETCHER_PROVIDERS = [ "zai", "glmt", "opencode-go", + "ollama-cloud", "minimax", "minimax-cn", "crof", diff --git a/open-sse/translator/helpers/schemaCoercion.ts b/open-sse/translator/helpers/schemaCoercion.ts index 9be3d930b6..60ecfa44a7 100644 --- a/open-sse/translator/helpers/schemaCoercion.ts +++ b/open-sse/translator/helpers/schemaCoercion.ts @@ -24,6 +24,18 @@ const NUMERIC_SCHEMA_FIELDS = [ "multipleOf", ] as const; +// Fix (9router#1556): OpenAI/Codex's Responses API rejects JSON Schema `pattern` +// values that use regex lookaround (lookahead/lookbehind) with +// "Invalid JSON schema: regex lookaround is not supported.". IDE/SDK agent +// harnesses commonly emit lookahead patterns (e.g. `^(?=.*@).+$`), so any +// `pattern` field containing `(?=`, `(?!`, `(?<=`, or `(? [key, stripUnsupportedRegexPatterns(value)]) + ); +} + +/** + * Strip regex `pattern` constraints that use lookaround (lookahead/lookbehind), + * which OpenAI/Codex's Responses API rejects outright with a 400 + * ("Invalid JSON schema: regex lookaround is not supported."). Walks the same + * JSON Schema shape as `coerceSchemaNumericFields` (properties, items, + * anyOf/oneOf/allOf, $defs/definitions, etc). See 9router#1556. + */ +export function stripUnsupportedRegexPatterns(schema: unknown): unknown { + if (Array.isArray(schema)) { + return schema.map((entry) => stripUnsupportedRegexPatterns(entry)); + } + if (!isPlainObject(schema)) return schema; + + const result: JsonRecord = { ...schema }; + + if (hasUnsupportedRegexLookaround(result.pattern)) { + delete result.pattern; + } + + for (const field of REGEX_STRIP_OBJECT_MAP_FIELDS) { + if (isPlainObject(result[field])) { + result[field] = stripRegexFromObjectMap(result[field]); + } + } + + for (const field of REGEX_STRIP_ARRAY_MAP_FIELDS) { + if (Array.isArray(result[field])) { + result[field] = (result[field] as unknown[]).map((entry) => + stripUnsupportedRegexPatterns(entry) + ); + } + } + + if (result.items !== undefined) { + result.items = stripUnsupportedRegexPatterns(result.items); + } + if (result.additionalProperties && typeof result.additionalProperties === "object") { + result.additionalProperties = stripUnsupportedRegexPatterns(result.additionalProperties); + } + if (isPlainObject(result.not)) { + result.not = stripUnsupportedRegexPatterns(result.not); + } + + return result; +} + export function sanitizeToolDescription(tool: unknown): unknown { if (!isPlainObject(tool)) return tool; diff --git a/open-sse/translator/request/claude-to-openai.ts b/open-sse/translator/request/claude-to-openai.ts index 9b8b70f3c7..ab50607e75 100644 --- a/open-sse/translator/request/claude-to-openai.ts +++ b/open-sse/translator/request/claude-to-openai.ts @@ -349,7 +349,15 @@ function fixMissingToolResponses(messages) { // Convert single Claude message - returns single message or array of messages function convertClaudeMessage(msg, preserveCacheControl = false) { - const role = msg.role === "user" || msg.role === "tool" ? "user" : "assistant"; + // Preserve system role for mid-conversation system turns (#6954). + // Previously any role that wasn't "user" or "tool" was mapped to "assistant", + // which misattributed system messages as assistant output. + const role = + msg.role === "user" || msg.role === "tool" + ? "user" + : msg.role === "system" + ? "system" + : "assistant"; // Simple string content if (typeof msg.content === "string") { @@ -411,9 +419,7 @@ function convertClaudeMessage(msg, preserveCacheControl = false) { function: { name: block.name, arguments: - typeof block.input === "string" - ? block.input - : JSON.stringify(block.input || {}), + typeof block.input === "string" ? block.input : JSON.stringify(block.input || {}), }, }); break; diff --git a/open-sse/translator/request/openai-to-claude.ts b/open-sse/translator/request/openai-to-claude.ts index ff5dc4b6d5..1cfa99ddff 100644 --- a/open-sse/translator/request/openai-to-claude.ts +++ b/open-sse/translator/request/openai-to-claude.ts @@ -592,7 +592,24 @@ function getContentBlocksFromMessage( if (part.type === "text" && part.text) { blocks.push({ type: "text", text: part.text }); } else if (part.type === "thinking" || part.type === "redacted_thinking") { - // Preserve thinking blocks with signature + // #6953 — thinking blocks with signature:"" (empty string) come from non-Anthropic + // providers (codex/gpt-5.x). Anthropic rejects replayed `thinking` blocks that + // carry a foreign or fabricated signature with HTTP 400. Fabricating a default + // signature (the old behaviour) made the poisoning permanent: once a codex-served + // turn introduced a `signature:""` thinking block, every subsequent Anthropic leg + // attempt 400'd and the router silently fell back to codex forever. + // + // Fix: strip thinking blocks whose signature is the empty string — that explicit + // empty value is the hallmark of a synthesized block from a non-Anthropic provider. + // Thinking blocks with `signature: undefined` (field absent) are legitimate Claude- + // format messages and fall through to the DEFAULT_THINKING_CLAUDE_SIGNATURE fallback + // as before. + if (part.type === "thinking" && part.signature === "") { + continue; // drop — synthesized by non-Anthropic provider, no valid signature + } + if (part.type === "redacted_thinking" && part.data === "") { + continue; // drop — same: empty data from non-Anthropic provider + } blocks.push({ ...part, signature: part.signature || DEFAULT_THINKING_CLAUDE_SIGNATURE, diff --git a/open-sse/translator/response/openai-responses.ts b/open-sse/translator/response/openai-responses.ts index 42fca56462..fdd214d06d 100644 --- a/open-sse/translator/response/openai-responses.ts +++ b/open-sse/translator/response/openai-responses.ts @@ -12,7 +12,7 @@ import { stripEmptyOptionalToolArgs, normalizeOutputIndex, normalizeUpstreamFailure, - extractResponsesReasoningSummaryText, + getVisibleResponsesReasoningSummaryText, } from "./openai-responses/pureHelpers.ts"; import { createEventEmitter } from "./openai-responses/eventEmitter.ts"; @@ -1070,7 +1070,11 @@ function openaiResponsesToOpenAIResponseStream(chunk, state) { !(state.reasoningItemsWithDelta instanceof Set && state.reasoningItemsWithDelta.size > 0); if (emittedForItem || emittedWithoutItemId) return null; - const summaryText = extractResponsesReasoningSummaryText(item); + // #7095/#7176 reconciliation: computed WITHOUT mutating `item`, so an + // encrypted-only reasoning item (and its `encrypted_content`) is never + // rewritten with a fabricated `summary` — the placeholder only feeds this + // synthetic client-facing delta chunk. + const summaryText = getVisibleResponsesReasoningSummaryText(item); if (!summaryText) return null; return buildResponsesReasoningDeltaChunk(state, summaryText); } diff --git a/open-sse/translator/response/openai-responses/pureHelpers.ts b/open-sse/translator/response/openai-responses/pureHelpers.ts index 094d7dc58d..50e9cfe6c6 100644 --- a/open-sse/translator/response/openai-responses/pureHelpers.ts +++ b/open-sse/translator/response/openai-responses/pureHelpers.ts @@ -163,3 +163,28 @@ export function extractResponsesReasoningSummaryText(item) { ) .join(""); } + +// #7095/#7176 — when Codex exposes a reasoning item only as encrypted private +// reasoning (no plaintext summary), chat clients would otherwise see nothing in +// their thinking panel. Reconciles two goals that used to be in tension: +// - #7095 wants a visible placeholder in the chat client. +// - #7176 wants the upstream response item left untouched, so `encrypted_content` +// (needed by Codex for subsequent requests) is never overwritten by a +// fabricated `summary`. +// This function computes the placeholder text WITHOUT mutating `item` — callers +// use the returned text for synthetic client-facing events only. +const ENCRYPTED_REASONING_PLACEHOLDER = + "Codex is reasoning, but the upstream Responses API exposed this reasoning block only as encrypted private reasoning. OmniRoute cannot recover the plaintext."; + +export function getVisibleResponsesReasoningSummaryText(item) { + const existingSummary = extractResponsesReasoningSummaryText(item); + if (existingSummary) return existingSummary; + + const hasEncryptedReasoning = + item && + item.type === "reasoning" && + typeof item.encrypted_content === "string" && + item.encrypted_content.length > 0; + + return hasEncryptedReasoning ? ENCRYPTED_REASONING_PLACEHOLDER : ""; +} diff --git a/open-sse/utils/composerToolCalls.ts b/open-sse/utils/composerToolCalls.ts index d688973ce3..916903a3ca 100644 --- a/open-sse/utils/composerToolCalls.ts +++ b/open-sse/utils/composerToolCalls.ts @@ -126,15 +126,28 @@ function parseInnerCall(body: string): { name: string; arguments: string } | nul const args: Record = {}; for (const seg of segments) { if (!seg) continue; - // Each segment is `arg_name\nvalue\n...`. The arg name is the first - // line; everything after the first newline is the value (verbatim, - // including additional newlines). + // Each segment is normally `arg_name\nvalue\n...`: the arg name is the + // first line, everything after the first newline is the value + // (verbatim, including additional newlines). Some live Composer/Auto + // captures instead separate the arg name and value with a single space + // on the same line (no newline at all in the segment) — fall back to + // splitting on the first whitespace boundary in that case so the value + // isn't swallowed into an empty-valued, space-containing "arg name". const idxNl = seg.indexOf("\n"); let argName: string; let argValue: string; if (idxNl < 0) { - argName = seg.trim(); - argValue = ""; + const idxSp = seg.search(/\s/); + if (idxSp < 0) { + argName = seg.trim(); + argValue = ""; + } else { + argName = seg.slice(0, idxSp).trim(); + // Unlike the newline-delimited form, a space-delimited value has no + // multi-line content to preserve — trim the trailing whitespace left + // over from the boundary with the next `<|tool▁sep|>` marker. + argValue = seg.slice(idxSp + 1).trim(); + } } else { argName = seg.slice(0, idxNl).trim(); argValue = seg.slice(idxNl + 1); diff --git a/open-sse/utils/earlyStreamKeepalive.ts b/open-sse/utils/earlyStreamKeepalive.ts index c70180f927..729049c9a3 100644 --- a/open-sse/utils/earlyStreamKeepalive.ts +++ b/open-sse/utils/earlyStreamKeepalive.ts @@ -28,6 +28,12 @@ const ENCODER = new TextEncoder(); const KEEPALIVE_FRAME = ENCODER.encode(": omniroute-keepalive\n\n"); +// OpenAI-compatible keepalive: a syntactically valid empty streaming chunk. +// Some OpenAI-compatible clients parse every non-empty SSE line as JSON and +// reject legal SSE comments before their first provider chunk arrives. +export const OPENAI_KEEPALIVE_FRAME = ENCODER.encode( + 'data: {"id":"omniroute-keepalive","object":"chat.completion.chunk","created":0,"model":"omniroute","choices":[{"index":0,"delta":{},"finish_reason":null}]}\n\n' +); // Anthropic Messages-format keepalive: a REAL `ping` SSE event, not a comment. // Anthropic clients (Claude Code, the Anthropic SDK) reset their stream/first-token // watchdog on real SSE events but ignore SSE comments (`: ...`), so on a slow first diff --git a/open-sse/utils/error.ts b/open-sse/utils/error.ts index 88efa6c67d..be3de82dda 100644 --- a/open-sse/utils/error.ts +++ b/open-sse/utils/error.ts @@ -145,6 +145,22 @@ function clampDiagStr(v: unknown, max = 128): string { return typeof v === "string" ? v.slice(0, max).replace(/[\r\n]+/g, " ") : ""; } +/** + * HTTP header values must be Latin1/ByteString (undici throws a TypeError + * otherwise — see #6612). Replace any codepoint outside the Latin1 range + * (0-255) with "?" so header construction never throws. Only used for the + * literal header value; the JSON body keeps the original, unsanitized + * readable text via `sanitizeComboDiagnostics`. + */ +function toHeaderSafeAscii(v: string): string { + let out = ""; + for (let i = 0; i < v.length; i++) { + const code = v.charCodeAt(i); + out += code > 255 ? "?" : v[i]; + } + return out; +} + /** * Whitelist projection — guarantees only id/reason string primitives + integer * counts can escape, regardless of what the caller assembled. This is the secret @@ -186,10 +202,12 @@ export function errorResponseWithComboDiagnostics( if (opts.code) body.error.code = opts.code; if (opts.type) body.error.type = opts.type; body.diagnostics = safe; - const excludedHeader = safe.excluded - .map((e) => `${e.provider}${e.model ? `/${e.model}` : ""}:${e.reason}`) - .join(",") - .slice(0, 900); + const excludedHeader = toHeaderSafeAscii( + safe.excluded + .map((e) => `${e.provider}${e.model ? `/${e.model}` : ""}:${e.reason}`) + .join(",") + .slice(0, 900) + ); return new Response(JSON.stringify(body), { status: statusCode, headers: { @@ -197,7 +215,7 @@ export function errorResponseWithComboDiagnostics( "x-omniroute-combo-pool-size": String(safe.poolSize), "x-omniroute-combo-attempted": String(safe.attempted), "x-omniroute-combo-excluded": excludedHeader, - "x-omniroute-combo-terminal-reason": safe.terminalReason.slice(0, 200), + "x-omniroute-combo-terminal-reason": toHeaderSafeAscii(safe.terminalReason.slice(0, 200)), }, }); } diff --git a/open-sse/utils/opencodeHeaders.ts b/open-sse/utils/opencodeHeaders.ts index 4e1221877c..8569c8512c 100644 --- a/open-sse/utils/opencodeHeaders.ts +++ b/open-sse/utils/opencodeHeaders.ts @@ -12,6 +12,15 @@ const OPENCODE_HEADER_KEYS = [ "x-opencode-client", ] as const; +/** + * Common agent-metadata headers used by non-OpenCode clients (custom agents/ + * providers) for upstream request tracking and attribution. Forwarded the same + * way as the x-opencode-* set: case-insensitive lookup, client value wins. + * Added for 9router#2413 — these were previously dropped for every client + * outside the OpenCode allowlist. + */ +const AGENT_METADATA_HEADER_KEYS = ["x-session-id", "x-title"] as const; + /** * Case-insensitive lookup for a header in a headers record. */ @@ -26,6 +35,8 @@ function findHeader(headers: Record, name: string): string | und * 1. Forwards User-Agent from clientHeaders via `setUserAgentHeader()` * 2. Forwards x-opencode-session, x-opencode-request, x-opencode-project, * x-opencode-client headers (case-insensitive match) + * 3. Forwards x-session-id, x-title agent-metadata headers (case-insensitive + * match) — common conventions used by non-OpenCode agent clients (9router#2413) * * @param headers - The outbound headers record to mutate * @param clientHeaders - The client-provided headers to forward from @@ -60,6 +71,14 @@ export function forwardOpencodeClientHeaders( } } + // 2b. Forward agent-metadata headers (x-session-id, x-title) — 9router#2413 + for (const headerName of AGENT_METADATA_HEADER_KEYS) { + const value = findHeader(clientHeaders, headerName); + if (value) { + headers[headerName] = value; + } + } + // 3. OpencodeExecutor-only: synthesize session/request id from fallback headers if (options?.synthesizeRequestId && !headers["x-opencode-session"]) { const sessionAffinity = diff --git a/open-sse/utils/passthroughTailProcessor.ts b/open-sse/utils/passthroughTailProcessor.ts index 82b57da8aa..845bc6e352 100644 --- a/open-sse/utils/passthroughTailProcessor.ts +++ b/open-sse/utils/passthroughTailProcessor.ts @@ -37,7 +37,6 @@ export type PassthroughTailProcessorContext = { appendPassthroughReasoning: (value: string) => void; getResponsesReasoningKey: (payload: Record) => string | null; markResponsesReasoningSummarySeen: (key: string) => void; - ensureVisibleResponsesReasoningSummary: (payload: Record) => boolean; emitSyntheticResponsesReasoningSummary: (payload: Record) => void; passthroughResponsesOutputItems: unknown[]; passthroughResponsesPendingFunctionCalls: Map; @@ -136,12 +135,8 @@ function handleResponsesTailPayload( } } if (parsed.type === "response.output_item.done" && parsed.item) { - const reasoningSummaryInjected = context.ensureVisibleResponsesReasoningSummary(parsed); context.emitSyntheticResponsesReasoningSummary(parsed); pushUniqueResponsesOutputItems(context.passthroughResponsesOutputItems, [parsed.item]); - if (reasoningSummaryInjected) { - output = `data: ${JSON.stringify(parsed)}\n\n`; - } const item = asRecord(parsed.item); if (item.type === "function_call") { const pendingKey = getFunctionCallPendingKey(item); diff --git a/open-sse/utils/reasoningContentInjector.ts b/open-sse/utils/reasoningContentInjector.ts index 8fd9b13bd4..c2e8318be4 100644 --- a/open-sse/utils/reasoningContentInjector.ts +++ b/open-sse/utils/reasoningContentInjector.ts @@ -1,5 +1,6 @@ /** - * Thinking-mode upstreams (DeepSeek V4 Flash, Kimi, MiniMax, ...) require + * Thinking-mode upstreams (DeepSeek V4 Flash, Kimi, MiniMax, xiaomi-tokenplan + * mimo, ...) require * `reasoning_content` to be echoed back on every assistant message in the * conversation history. Standard OpenAI clients do not preserve that field * across turns, so we inject a non-empty placeholder before forwarding. @@ -26,6 +27,7 @@ const THINKING_MODEL_PATTERNS: RegExp[] = [ /\bkimi\b/i, /\bk2\b/i, // moonshot kimi k2 family alias /\bminimax\b/i, + /\bmimo\b/i, // xiaomi-tokenplan mimo family (e.g. xiaomi-tokenplan/mimo-v2.5-pro) ]; export function isThinkingMessageModel(model: string | undefined | null): boolean { diff --git a/open-sse/utils/stream.ts b/open-sse/utils/stream.ts index 7ec442163e..974d532e9e 100644 --- a/open-sse/utils/stream.ts +++ b/open-sse/utils/stream.ts @@ -52,6 +52,7 @@ import { stripResponsesLifecycleEcho, } from "./responsesStreamHelpers.ts"; import { processBufferedPassthroughLine } from "./passthroughTailProcessor.ts"; +import { getVisibleResponsesReasoningSummaryText } from "../translator/response/openai-responses/pureHelpers.ts"; import { getAnyReasoningValue, getReadableReasoningValue, @@ -1006,49 +1007,6 @@ export function createSSEStream(options: StreamOptions = {}) { return responseId !== null && outputIndex !== null ? `${responseId}:${outputIndex}` : null; }; - const getResponsesReasoningSummaryText = (item: Record): string => { - return Array.isArray(item.summary) - ? item.summary - .map((part) => { - if (!part || typeof part !== "object" || Array.isArray(part)) { - return ""; - } - return typeof (part as Record).text === "string" - ? ((part as Record).text as string) - : ""; - }) - .join("") - : ""; - }; - - const ensureVisibleResponsesReasoningSummary = (payload: Record): boolean => { - const item = - payload.item && typeof payload.item === "object" && !Array.isArray(payload.item) - ? (payload.item as Record) - : null; - if (!item || item.type !== "reasoning") { - return false; - } - - if (getResponsesReasoningSummaryText(item)) { - return false; - } - - const hasEncryptedReasoning = - typeof item.encrypted_content === "string" && item.encrypted_content.length > 0; - if (!hasEncryptedReasoning) { - return false; - } - - item.summary = [ - { - type: "summary_text", - text: "Codex is reasoning, but the upstream Responses API exposed this reasoning block only as encrypted state. OmniRoute cannot recover the private reasoning text.", - }, - ]; - return true; - }; - const emitSyntheticResponsesReasoningSummary = ( controller: TransformStreamDefaultController, payload: Record @@ -1061,8 +1019,10 @@ export function createSSEStream(options: StreamOptions = {}) { return; } - ensureVisibleResponsesReasoningSummary(payload); - const visibleSummary = getResponsesReasoningSummaryText(item); + // #7095/#7176 reconciliation: compute the visible placeholder WITHOUT + // mutating `item` — the encrypted reasoning item (and its `encrypted_content`, + // required by Codex for subsequent requests) is forwarded to the client intact. + const visibleSummary = getVisibleResponsesReasoningSummaryText(item); if (!visibleSummary) { return; @@ -1485,13 +1445,8 @@ export function createSSEStream(options: StreamOptions = {}) { // response.completed snapshot can be backfilled when upstream // returns an empty `output` (happens with store: false). if (parsed.type === "response.output_item.done" && parsed.item) { - const reasoningSummaryInjected = ensureVisibleResponsesReasoningSummary(parsed); emitSyntheticResponsesReasoningSummary(controller, parsed); pushUniqueResponsesOutputItems(passthroughResponsesOutputItems, [parsed.item]); - if (reasoningSummaryInjected) { - output = `data: ${JSON.stringify(parsed)}\n\n`; - injectedUsage = true; - } if (parsed.item?.type === "function_call") { const pendingKey = typeof parsed.item.id === "string" @@ -1710,7 +1665,7 @@ export function createSSEStream(options: StreamOptions = {}) { const reasoningChunk = typeof structuredClone === "function" ? structuredClone(parsed) - : JSON.parse(JSON.stringify(parsed)); + : structuredClone(parsed); const rDelta = reasoningChunk.choices[0].delta; delete rDelta.content; reasoningChunk.choices[0].finish_reason = null; @@ -2181,7 +2136,6 @@ export function createSSEStream(options: StreamOptions = {}) { markResponsesReasoningSummarySeen: (key: string) => { passthroughResponsesReasoningSummarySeen.add(key); }, - ensureVisibleResponsesReasoningSummary, emitSyntheticResponsesReasoningSummary: (payload: Record) => emitSyntheticResponsesReasoningSummary(controller, payload), passthroughResponsesOutputItems, diff --git a/package-lock.json b/package-lock.json index ba6fe54b8b..a678e41868 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "omniroute", - "version": "3.8.48", + "version": "3.8.49", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "omniroute", - "version": "3.8.48", + "version": "3.8.49", "hasInstallScript": true, "license": "MIT", "workspaces": [ @@ -114,21 +114,26 @@ "c8": "^11.0.0", "concurrently": "^10.0.3", "cross-env": "^10.1.0", + "ctrf": "^0.2.1", "dpdm": "^4.2.0", "eslint": "^9.39.4", "eslint-config-next": "16.2.10", "eslint-plugin-sonarjs": "^4.1.0", "fast-check": "^4.8.0", "glob": "^13.0.6", + "httpyac": "^6.16.7", "husky": "^9.1.7", "jscpd": "^4.2.5", "jsdom": "^29.1.1", + "junit-to-ctrf": "^0.0.14", "knip": "^6.18.0", "license-checker-rseidelsohn": "^5.0.1", "lint-staged": "^17.0.8", "lockfile-lint": "^5.0.0", "node-loader": "^2.1.0", + "playwright-ctrf-json-reporter": "^0.0.29", "prettier": "^3.8.3", + "promptfoo": "^0.121.18", "size-limit": "^12.1.0", "tailwindcss": "^4.3.0", "type-coverage": "^2.29.7", @@ -159,6 +164,66 @@ "dev": true, "license": "MIT" }, + "node_modules/@ai-sdk/gateway": { + "version": "3.0.149", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.149.tgz", + "integrity": "sha512-+EVPEHqdJVJn0FZHBd6NyH4rvlTK7X79B6xFuW5bfZIP1G/7Y5OTEgxpL0hjOCAxDBG4ZFM6SZWnVBXxeR1x8w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.14", + "@ai-sdk/provider-utils": "4.0.38", + "@vercel/oidc": "3.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/provider": { + "version": "3.0.14", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.14.tgz", + "integrity": "sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/provider-utils": { + "version": "4.0.38", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.38.tgz", + "integrity": "sha512-/HHGmtKllqjg1OLc023v9w9kK3laW7Z6TzfZukYQWCsGBbzB9p60zTvvpXFVcs44NZBVXL3viOa1HRKUbeee8g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.14", + "@standard-schema/spec": "^1.1.0", + "eventsource-parser": "^3.0.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-zen/node-fetch-event-source": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@ai-zen/node-fetch-event-source/-/node-fetch-event-source-2.1.4.tgz", + "integrity": "sha512-OHFwPJecr+qwlyX5CGmTvKAKPZAdZaxvx/XDqS1lx4I2ZAk9riU0XnEaRGOOAEFrdcLZ98O5yWqubwjaQc0umg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "cross-fetch": "^4.0.0" + } + }, "node_modules/@alcalzone/ansi-tokenize": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.3.0.tgz", @@ -229,6 +294,217 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/@anthropic-ai/claude-agent-sdk": { + "version": "0.3.195", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.195.tgz", + "integrity": "sha512-FVmXu9pvOMbuBKWrF8YsYQdQ/upOpv5rS8lFAnFO5jbyXT/2hN7kEPd2vd2GJpaMvNcO/KptyQUK5AxjjTz3+w==", + "dev": true, + "license": "SEE LICENSE IN README.md", + "optional": true, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.195", + "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.195", + "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.195", + "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.195", + "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.195", + "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.195", + "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.195", + "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.195" + }, + "peerDependencies": { + "@anthropic-ai/sdk": ">=0.93.0", + "@modelcontextprotocol/sdk": "^1.29.0", + "zod": "^4.0.0" + } + }, + "node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64": { + "version": "0.3.195", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.195.tgz", + "integrity": "sha512-WIMM/8HRCLsTDHFTIwQvvE8WCA/oaMJtdQxsP7iNyfzIGwXbuOyU95V8vYIhZfaO2yaSpbBRncunq4CtR5H4ng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-darwin-x64": { + "version": "0.3.195", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.195.tgz", + "integrity": "sha512-RY7DB+4LXosE0MJ+XELmakfPrDN1YX4lkk9CTDm28jGCVcESRz9kAEqbyaiC48dZcmN9V1NCLutzINGdcr1TBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64": { + "version": "0.3.195", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.195.tgz", + "integrity": "sha512-JuIq5Fnz/F1snl0aqi1gcuRZqPWoPNrL9dJ0DuievCxKkO8hnEz/Mmn5Zos7x1X8HE//ZnEvmQXoEQEZXonJew==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64-musl": { + "version": "0.3.195", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.195.tgz", + "integrity": "sha512-ZmyBA/AFzhgutcxb7dbhCm6GTjJytwNYXTxJoKE2B3A409WCYccjMqeji6vCMNxyyfylglGo5D8dVMIxW9aoug==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-linux-x64": { + "version": "0.3.195", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.195.tgz", + "integrity": "sha512-s1lNi1cL93luoqsItH+fNO4KpIhdkvnVhWGGQUQ/8ftwa2gfmcIQnOg1hG8Ks+KzeD3UUQ8L9YEVHVADnFI/9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl": { + "version": "0.3.195", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.195.tgz", + "integrity": "sha512-nf8Q/LauB+ZOC6QDjxNhbsvwUtYjKYnaWJLTYFwhkmsLujePnety1AtT/1ubaUoq5AM1j297DhMlYTasa79OUA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-win32-arm64": { + "version": "0.3.195", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.195.tgz", + "integrity": "sha512-hbkDE+xPIZzRWm+D+BKrH9uJH6USIZdDIlsyrIlGi3JFHoieYoA1vdUNyldSS9+F3ZqQtfPjr2Qy08IVB6akYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-win32-x64": { + "version": "0.3.195", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.195.tgz", + "integrity": "sha512-av0piEB3X1Dzhpr8A+DqHVZ9y8s1jpn8enzwX0TKKUPBn5IqLTWC7wD6v66aoUgu4f+g4ThZirmDZA6shyPEZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.106.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.106.0.tgz", + "integrity": "sha512-ufwVvYNDBj2dzOGupBCTaNzBLxqcTnGOzI4z8Wouxlt+mT3J3HuOmatgCy1VmwCHOUueqZ41ERhm0O99OUcbWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1", + "standardwebhooks": "^1.0.0" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "15.5.0", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-15.5.0.tgz", + "integrity": "sha512-Ps4w0FwrDoeVK6hfYxWkVbkmxm+zN+6xoXF2ZfEhfiox0ZNbcSAiUWO6iAIvP5bc3DB270r+EaKcoT1IUyzfxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-yaml": "^4.2.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@types/json-schema": "^7.0.15" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser/node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/@asamuzakjp/css-color": { "version": "5.1.11", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", @@ -295,6 +571,45 @@ "js-tiktoken": "*" } }, + "node_modules/@aws-sdk/checksums": { + "version": "3.1000.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/checksums/-/checksums-3.1000.16.tgz", + "integrity": "sha512-EKnvkXSmz3IpA99tCNuI+dLFXyZyClSm8zns9sB/elvkU+MTuomAs6toJMPMBf98/fICG/urXDkzGz0/c3yyAQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-bedrock-agent-runtime": { + "version": "3.1086.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-agent-runtime/-/client-bedrock-agent-runtime-3.1086.0.tgz", + "integrity": "sha512-urlsx3VdXU+FP0vs88bTNDbqtv6DKoDTkINo5Bts3apIeN6JW0pamjF9PmyJsenTLQvbHB6lkTHicar5CEp9og==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/credential-provider-node": "^3.972.67", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/fetch-http-handler": "^5.6.4", + "@smithy/node-http-handler": "^4.9.4", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/client-bedrock-runtime": { "version": "3.1081.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1081.0.tgz", @@ -318,18 +633,63 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.1086.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1086.0.tgz", + "integrity": "sha512-6+7mVMPKetZmmF2L1yRJ+rN9b1OwVgc5sju2mj8ixdxuGjtVZ0ekFlcWGBFMQT9gpFk55PPLBng/XRYO3qah5A==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@aws-sdk/checksums": "^3.1000.16", + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/credential-provider-node": "^3.972.67", + "@aws-sdk/middleware-sdk-s3": "^3.972.62", + "@aws-sdk/signature-v4-multi-region": "^3.996.39", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/fetch-http-handler": "^5.6.4", + "@smithy/node-http-handler": "^4.9.4", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-sagemaker-runtime": { + "version": "3.1086.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sagemaker-runtime/-/client-sagemaker-runtime-3.1086.0.tgz", + "integrity": "sha512-E37Ros1JssB5+hGtUz9yhjyxcZ+vH6rxentCXMerXqBfh/wnQTDxR4LESLwruXeDwfMupzySZRwvj8y+kBaSXA==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/credential-provider-node": "^3.972.67", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/fetch-http-handler": "^5.6.4", + "@smithy/node-http-handler": "^4.9.4", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/core": { - "version": "3.974.29", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.29.tgz", - "integrity": "sha512-yqKcltLbtRh1ubzhRSldIs8jFHNZlyMlgoIccCC0aDVbrB99nXaBdmfr89mK7obWX/NVg4rAMpCpZ6dCDiVBtA==", + "version": "3.975.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.975.1.tgz", + "integrity": "sha512-8qh/6EYb7hl/ZwVfQufhbMEZs1gQIc7GbdrIf4eprQJ7cv042+74nE6l3YDfyWNzb9iPXb8fRyYSHkNIk5eE6Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.15", - "@aws-sdk/xml-builder": "^3.972.33", + "@aws-sdk/types": "^3.974.0", + "@aws-sdk/xml-builder": "^3.972.34", "@aws/lambda-invoke-store": "^0.3.0", - "@smithy/core": "^3.29.0", - "@smithy/signature-v4": "^5.6.1", - "@smithy/types": "^4.15.1", + "@smithy/core": "^3.29.2", + "@smithy/signature-v4": "^5.6.3", + "@smithy/types": "^4.16.0", "bowser": "^2.11.0", "tslib": "^2.6.2" }, @@ -338,15 +698,15 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.55", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.55.tgz", - "integrity": "sha512-Ah36tYkqyaVnaHkx7VseoTYrHUmwgBps3V+wnrC1idhIIMGlviH0FtrX9EIPdAlVHvXC7FQZLhmHBRz+pLaiWg==", + "version": "3.972.57", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.57.tgz", + "integrity": "sha512-1RfJaF7SW1TOnvNGU7kaYjwUf5H3sfm+synGH1bHhRlqcnxCt3szebH3dmKEyY4tuGcbQ6ffzUT89cRitBV8OQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.29", - "@aws-sdk/types": "^3.973.15", - "@smithy/core": "^3.29.0", - "@smithy/types": "^4.15.1", + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", "tslib": "^2.6.2" }, "engines": { @@ -354,17 +714,17 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.57", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.57.tgz", - "integrity": "sha512-/vp6i5YEliJqRm5k/BDmYjAyRAMTdkjW6UciVRk9oh/0OfDCWeb/ih7hqte4lFvKXkIbsqe9AdK9LQK6NGardw==", + "version": "3.972.59", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.59.tgz", + "integrity": "sha512-sRCkpTiFnCdQvuaRVjQ6SVoHu6i7RUpurVo1c4F81HWhPvUJ7Wdp5MNtSdX1O29CNXc8em3O5m52hCjVtAD9SA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.29", - "@aws-sdk/types": "^3.973.15", - "@smithy/core": "^3.29.0", - "@smithy/fetch-http-handler": "^5.6.2", - "@smithy/node-http-handler": "^4.9.2", - "@smithy/types": "^4.15.1", + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/fetch-http-handler": "^5.6.4", + "@smithy/node-http-handler": "^4.9.4", + "@smithy/types": "^4.16.0", "tslib": "^2.6.2" }, "engines": { @@ -372,23 +732,23 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.62", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.62.tgz", - "integrity": "sha512-pQIRiQQs+MUlVnJdWJ7/6KS0WxcLRVfut57OFgwC3cnM1F8mXw3Kh4gAVwj6AtvD6CWx8x6+po4ENRcqe64XrQ==", + "version": "3.973.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.1.tgz", + "integrity": "sha512-6d8H6ZAh3ZPKZ6fe1nG2OWeZEZPtt9ravoD1dezPdPtsSkJRoxGAnFSHwKT3E/Te6fHE30zRzjV6TD12rvF6yQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.29", - "@aws-sdk/credential-provider-env": "^3.972.55", - "@aws-sdk/credential-provider-http": "^3.972.57", - "@aws-sdk/credential-provider-login": "^3.972.61", - "@aws-sdk/credential-provider-process": "^3.972.55", - "@aws-sdk/credential-provider-sso": "^3.972.61", - "@aws-sdk/credential-provider-web-identity": "^3.972.61", - "@aws-sdk/nested-clients": "^3.997.29", - "@aws-sdk/types": "^3.973.15", - "@smithy/core": "^3.29.0", - "@smithy/credential-provider-imds": "^4.4.5", - "@smithy/types": "^4.15.1", + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/credential-provider-env": "^3.972.57", + "@aws-sdk/credential-provider-http": "^3.972.59", + "@aws-sdk/credential-provider-login": "^3.972.63", + "@aws-sdk/credential-provider-process": "^3.972.57", + "@aws-sdk/credential-provider-sso": "^3.973.1", + "@aws-sdk/credential-provider-web-identity": "^3.972.63", + "@aws-sdk/nested-clients": "^3.997.31", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/credential-provider-imds": "^4.4.7", + "@smithy/types": "^4.16.0", "tslib": "^2.6.2" }, "engines": { @@ -396,16 +756,16 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.61", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.61.tgz", - "integrity": "sha512-jtrxWwC7slqxh7DnAWHrwsA3UwCsnlypdYtavGT7EX5p791wxWQys7QzkCZ7JvOMAyylDtPoxyV+ic0zg3rV9g==", + "version": "3.972.63", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.63.tgz", + "integrity": "sha512-GREWRrMj0XnNKMaVa/Mauoaui26qBEHu71WWqXbwZOu/jFQOnPZjTf7u0KtGKC8VGa6VUs9kDWGgocrKNLS9vw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.29", - "@aws-sdk/nested-clients": "^3.997.29", - "@aws-sdk/types": "^3.973.15", - "@smithy/core": "^3.29.0", - "@smithy/types": "^4.15.1", + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/nested-clients": "^3.997.31", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", "tslib": "^2.6.2" }, "engines": { @@ -413,21 +773,21 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.64", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.64.tgz", - "integrity": "sha512-zyKVYDyMR9VQL/kPi03ygN2vtD9uLMuWRLoJ77KxgZZaS1VlJloI+SzleF9Zg4HWUI+AIu+ZRs8zsJFNqbrxsw==", + "version": "3.972.67", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.67.tgz", + "integrity": "sha512-oYlzWst56rlhhjbYnexwv5hVLYe1cW4liLObhDfxDLI4RAQzleMVHQgQgx7XsC4HKj4e3kjT8v9DId+Pi/dndw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.55", - "@aws-sdk/credential-provider-http": "^3.972.57", - "@aws-sdk/credential-provider-ini": "^3.972.62", - "@aws-sdk/credential-provider-process": "^3.972.55", - "@aws-sdk/credential-provider-sso": "^3.972.61", - "@aws-sdk/credential-provider-web-identity": "^3.972.61", - "@aws-sdk/types": "^3.973.15", - "@smithy/core": "^3.29.0", - "@smithy/credential-provider-imds": "^4.4.5", - "@smithy/types": "^4.15.1", + "@aws-sdk/credential-provider-env": "^3.972.57", + "@aws-sdk/credential-provider-http": "^3.972.59", + "@aws-sdk/credential-provider-ini": "^3.973.1", + "@aws-sdk/credential-provider-process": "^3.972.57", + "@aws-sdk/credential-provider-sso": "^3.973.1", + "@aws-sdk/credential-provider-web-identity": "^3.972.63", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/credential-provider-imds": "^4.4.7", + "@smithy/types": "^4.16.0", "tslib": "^2.6.2" }, "engines": { @@ -435,15 +795,15 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.55", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.55.tgz", - "integrity": "sha512-x0XjjF0l1WGRtK2vEhTZqCguQuAIZLep9l2+eeEmuxQQjjD3BlGQXY5xADR+l3t576UX+dxRkRtTjEu40l81Vw==", + "version": "3.972.57", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.57.tgz", + "integrity": "sha512-TiVQhuU0pbhIZAUZacbPHMyzrIdiH+lnx+PMY/Pu/b93dJrq3wdZwzUJ0TPpvNxaqbHsxJvQZW3/h/beLiKq7Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.29", - "@aws-sdk/types": "^3.973.15", - "@smithy/core": "^3.29.0", - "@smithy/types": "^4.15.1", + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", "tslib": "^2.6.2" }, "engines": { @@ -451,17 +811,34 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.61", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.61.tgz", - "integrity": "sha512-d/V0VRsz73i+PHhbult/tx0Y1+de1SNQVsXkcQCmpfeBq7uODy/RTxNsOLpT9ZVHxcRNzbQFuywLKC33fUMIxA==", + "version": "3.973.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.1.tgz", + "integrity": "sha512-3foTZUJ4821Ij60X7K3NJroygiZLnbBmarN+T//O2cjkISan90zElN3NBmgSlDrTQ7Gs6z/yO8V7h60QNcDZHQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.29", - "@aws-sdk/nested-clients": "^3.997.29", - "@aws-sdk/token-providers": "3.1081.0", - "@aws-sdk/types": "^3.973.15", - "@smithy/core": "^3.29.0", - "@smithy/types": "^4.15.1", + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/nested-clients": "^3.997.31", + "@aws-sdk/token-providers": "3.1083.0", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { + "version": "3.1083.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1083.0.tgz", + "integrity": "sha512-s0woKnxuHrExLc5L2ArIH5BMkbonHPtt+5hSBM8oknp9M6QTuUmmAmJ2E0EdzCGONrO+8+ADPqvv6UX0nNcc7A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/nested-clients": "^3.997.31", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", "tslib": "^2.6.2" }, "engines": { @@ -469,16 +846,16 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.61", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.61.tgz", - "integrity": "sha512-Bv4n3NOI6hPy+rmr6Bw9R6LnBVRkcp3ncj2E2IKSYJG+0UkysSitWMvbgndNvMxDw7gE1pQ/ErwkNceuKwj7zQ==", + "version": "3.972.63", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.63.tgz", + "integrity": "sha512-8qZLFhM69eKcS37m459ctPR05Qimycm/74OPVioe6wNZabMT54GYhwBju0+J656RkMasNSawWQu+c8CmBe3TUQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.29", - "@aws-sdk/nested-clients": "^3.997.29", - "@aws-sdk/types": "^3.973.15", - "@smithy/core": "^3.29.0", - "@smithy/types": "^4.15.1", + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/nested-clients": "^3.997.31", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", "tslib": "^2.6.2" }, "engines": { @@ -515,6 +892,25 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.972.62", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.62.tgz", + "integrity": "sha512-k8JJwYXVYlOOjWnPZDThQS1xDFJgi5Dokt73qFlDtrZAbdcint5aIdjB9XgJAAQVP5OoqcefQmh1FYXiPpvsvw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/signature-v4-multi-region": "^3.996.39", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/middleware-websocket": { "version": "3.972.37", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.37.tgz", @@ -534,18 +930,18 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.997.29", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.29.tgz", - "integrity": "sha512-ot6v8J5W8P0w6ryyuIkXP1bHZHTlvwtn83mVCYaBE0GJ6tJX4vPSBx7M98w9O4wmmDruFsDBUMjhEHA+OosUFQ==", + "version": "3.997.31", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.31.tgz", + "integrity": "sha512-BDHTpwcsZHEBNEJzOg/B1BkFYJxAXY50dau/NyVWs3d51F0WgIUGSWZot/Os+N3KpDhXeaXnz37mWffAvduREw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.29", - "@aws-sdk/signature-v4-multi-region": "^3.996.38", - "@aws-sdk/types": "^3.973.15", - "@smithy/core": "^3.29.0", - "@smithy/fetch-http-handler": "^5.6.2", - "@smithy/node-http-handler": "^4.9.2", - "@smithy/types": "^4.15.1", + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/signature-v4-multi-region": "^3.996.39", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/fetch-http-handler": "^5.6.4", + "@smithy/node-http-handler": "^4.9.4", + "@smithy/types": "^4.16.0", "tslib": "^2.6.2" }, "engines": { @@ -553,14 +949,14 @@ } }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.996.38", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.38.tgz", - "integrity": "sha512-C379Sk+MiFZCfWZphKlMyLHKxV22OjoGM5KJjj5IJNJcOCWL4IGIpnEGzv1FQiRwhYXfq55SJMfxlqPE08JJ9g==", + "version": "3.996.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.39.tgz", + "integrity": "sha512-8+srXqYIF8KYMLC4FxMLEM5Ek7kUNibJu1R4m8/fUhhNYIZZz26oGtKkCr8I/HiG2fFQxBvaGgQZT4/mqRCSnA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.15", - "@smithy/signature-v4": "^5.6.1", - "@smithy/types": "^4.15.1", + "@aws-sdk/types": "^3.974.0", + "@smithy/signature-v4": "^5.6.3", + "@smithy/types": "^4.16.0", "tslib": "^2.6.2" }, "engines": { @@ -585,12 +981,12 @@ } }, "node_modules/@aws-sdk/types": { - "version": "3.973.15", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.15.tgz", - "integrity": "sha512-IULn8uBV/SMtmOIANsm4WHXIOtVPBWfOWs3WGL0j/sI+KhaYehvOw0ET+9urnn8MBpiijuU/0JOpuwKOE451PQ==", + "version": "3.974.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.0.tgz", + "integrity": "sha512-QIBrw90CDm4O0UaIIzkU6DrFdeJzEb2Va5EPEVpyldj6sHJxB6cshhStJuhZxk3wR3PmjJlYsjPmY1kNb+KGBg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.15.1", + "@smithy/types": "^4.16.0", "tslib": "^2.6.2" }, "engines": { @@ -598,12 +994,12 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.33", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.33.tgz", - "integrity": "sha512-ezbwz9WpuLctm6o7P2t2naDhVVPI5jFGrVefVybhcKGjU57VIyT46pQVO0RI2RYkUdhdj2Z9uSIlAzGZE9NW9A==", + "version": "3.972.34", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.34.tgz", + "integrity": "sha512-wHhWL1y7sN3enBA8POrPpQM5jCcmu2ozyhbRei4c8OjVcEaEs6yLucLa/pla457ggS/ysuy7bosagz3HaJkZXA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.15.1", + "@smithy/types": "^4.16.0", "tslib": "^2.6.2" }, "engines": { @@ -632,6 +1028,451 @@ "playwright-core": ">= 1.0.0" } }, + "node_modules/@azure-rest/core-client": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@azure-rest/core-client/-/core-client-2.8.0.tgz", + "integrity": "sha512-F1ybHeN+++QhyFCF/ehLUEvrOB6fehPdFBFtGdj0C3B2lpQ9zkPiO5JDgsqc6IfjuUe6b3dAbXK0a7+VgSGfhw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.24.0", + "@azure/core-tracing": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/abort-controller": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.2.0.tgz", + "integrity": "sha512-fNAjWnA/nZ2jz31kxR/AqRaUT8ewHBw/WuBIosK0moMy1C9e5ValbDfFdIxJzVOOYaYkV/b2F1S4H/aHiqfVQg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/ai-projects": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@azure/ai-projects/-/ai-projects-2.3.1.tgz", + "integrity": "sha512-xrBy4UQqx5nhV4xx2dFdEMWFPSiwDHHDYKBkKXNTMn5SXyBujvjxBDU3w9c4sH9KDeMxSaGUP1dbNfMcgL04fQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@azure-rest/core-client": "^2.1.0", + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.6.0", + "@azure/core-lro": "^3.1.0", + "@azure/core-paging": "^1.5.0", + "@azure/core-rest-pipeline": "^1.5.0", + "@azure/core-sse": "^2.1.3", + "@azure/core-util": "^1.9.0", + "@azure/identity": "^4.13.0", + "@azure/logger": "^1.1.4", + "@azure/storage-blob": "^12.26.0", + "@opentelemetry/api": "^1.9.0", + "openai": "^6.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.11.0.tgz", + "integrity": "sha512-IUZydyTUkDnYdstOW9pFOOUQlBjAepK5teihDE3x6yxsPJs/hsAaaYpeGxdxrgtOiJbBKSjKW7MDk7AEhb4LRg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.11.0.tgz", + "integrity": "sha512-JjQWO6akOck45PH/XBrxzsQGAiKrfFl4m5iggJ0ItMIz5omRufOXWpqCPpdjKN3vKDzlSUvFjaMb7Zwf0gvAdA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-http-compat": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.5.0.tgz", + "integrity": "sha512-BoSmXPx2er1Ai+wKlDvj29jIQespCNBwEmKyZVHO2kEFsWbGjAjwMCGzug3DJM5/QYIV3vej0S1zcU5bq9fa8w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@azure/abort-controller": "^2.1.2" + }, + "engines": { + "node": ">=22.0.0" + }, + "peerDependencies": { + "@azure/core-client": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0" + } + }, + "node_modules/@azure/core-lro": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-3.4.0.tgz", + "integrity": "sha512-y0uqcVFp5NHd7tkZcn8Nes6yIhVR05m4dd+L8foWiH1IsS75Z2BodJxwdErEF3bV+NSh6nkNnwPyXaLp0ma1Nw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-paging": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.7.0.tgz", + "integrity": "sha512-7GEAoIsaoBr6KELNRb8nypowCqvk8dnCHFCYg4XD4lOQGY2GqjQg5IhkRjyBFRO18CGSMq05PaNqSOE9GQro3g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.25.0.tgz", + "integrity": "sha512-bMs8ekJLjX8wPV+9IPBges1SLPyuDtE9g5gLDWOpxzKcoOFQnpLGkbcT1tdw3FaAmDS1gnPmMmJ6y/T5B96kIA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-sse": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@azure/core-sse/-/core-sse-2.4.0.tgz", + "integrity": "sha512-BFNVsoYE843I/q5/OFNHpaYN8TK8W99OwU9ipToYXdwB14o92A16ZjD6JW/BZ8kO7fWSM4jK2EoojAQmXAOExw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.4.0.tgz", + "integrity": "sha512-eGwxD0AtncrxeBM4tG8R55Pc3rdX1hNW2WibJAgYpCVA6E93mvvVH+LcssoVjOBrSKWS55yEIHsk0X8ctHmfOQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.14.0.tgz", + "integrity": "sha512-9n2pWK61veAuN0V20t9lOuoV4CFMdyAZ1ygZzvBGk/pBBJRib/PjL9PLXa/aI2CcPpyHfqVsxxqLCYl6uZlfDw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-xml": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@azure/core-xml/-/core-xml-1.6.0.tgz", + "integrity": "sha512-e7lX/dk//F6Qf7BB6PTY4+p2yuOQtyOeHGyapYHNwqSp2OnYpwQt49A/Nin2XmKBQ69pwagR4k/lQBq8lbHQkA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "fast-xml-parser": "^5.5.9", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.1.tgz", + "integrity": "sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^5.5.0", + "@azure/msal-node": "^5.1.0", + "open": "^10.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/identity/node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@azure/identity/node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@azure/logger": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.4.0.tgz", + "integrity": "sha512-rbAE25KUfjU/s3XHUdJgceoCP5dEOpMx85J04kF+QMdta73XkuG9JGHHinch+XIoKpBdqljin+KqURpJriSzLA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.17.0.tgz", + "integrity": "sha512-/yTnW2TCk9Mh+2b/NOaHAN+MryUNxzRTaJD/YtrqOA9bpBWfTXn/iyReRbaLrK/btBo3stEzLyEvuWp2NZ5DuA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@azure/msal-common": "16.11.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "16.11.1", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.11.1.tgz", + "integrity": "sha512-yPohvMwWLv1XnaWnIUyKUh8CvcVChCGqG/VluGwfGmaAfrZTNt5yQ+sIs462Sgw6+e2K83KGmMJ860p73ZSCrw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.4.0.tgz", + "integrity": "sha512-6EZEParwHRlnSSIikw8FNAnAzwmh71uhveUXdPNFeZFviJ9SH+rwFiurhjzXqICYTrpm3E+dj693QOwfPbJXAQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@azure/msal-common": "16.11.1", + "jsonwebtoken": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@azure/openai-assistants": { + "version": "1.0.0-beta.6", + "resolved": "https://registry.npmjs.org/@azure/openai-assistants/-/openai-assistants-1.0.0-beta.6.tgz", + "integrity": "sha512-gINKKcqTpR0neF+36Owe0Q1u1JO3IK6clBzWTfZ+9V/TkQq+LoUgp5F8dKvSv/YChfwEpZA2r1DWCwNE07eYIQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@azure-rest/core-client": "^1.1.4", + "@azure/core-auth": "^1.5.0", + "@azure/core-client": "^1.7.3", + "@azure/core-rest-pipeline": "^1.13.0", + "@azure/core-util": "^1.6.1", + "@azure/logger": "^1.0.4", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/openai-assistants/node_modules/@azure-rest/core-client": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@azure-rest/core-client/-/core-client-1.4.0.tgz", + "integrity": "sha512-ozTDPBVUDR5eOnMIwhggbnVmOrka4fXCs8n8mvUo4WLLc38kki6bAOByDoVZZPz/pZy2jMt2kwfpvy/UjALj6w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-rest-pipeline": "^1.5.0", + "@azure/core-tracing": "^1.0.1", + "@azure/core-util": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/storage-blob": { + "version": "12.33.0", + "resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.33.0.tgz", + "integrity": "sha512-2SX8oP8PyblUcAFZSg39c8Ls+tFjavM6sBeV+qpw33mRzRhI/5hrFJmJ/x0H9xx5l6ECPvgSP8uPxqTeVbHNIA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.3", + "@azure/core-http-compat": "^2.2.0", + "@azure/core-lro": "^2.2.0", + "@azure/core-paging": "^1.6.2", + "@azure/core-rest-pipeline": "^1.19.1", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/core-xml": "^1.4.5", + "@azure/logger": "^1.1.4", + "@azure/storage-common": "^12.4.1", + "events": "^3.0.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/storage-blob/node_modules/@azure/core-lro": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz", + "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.2.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/storage-common": { + "version": "12.4.1", + "resolved": "https://registry.npmjs.org/@azure/storage-common/-/storage-common-12.4.1.tgz", + "integrity": "sha512-t14unw/WofGDUi7TKJrsyXyPsN+NLgRm7hMaq0llxNmTIzt7f257+6LE6FKIJPh88zLj6M7LPvzve0fEYg/L3A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-http-compat": "^2.2.0", + "@azure/core-rest-pipeline": "^1.24.0", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.1.4", + "events": "^3.3.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -1086,9 +1927,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", - "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1149,6 +1990,18 @@ "node": ">=18" } }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "dev": true, + "license": "MIT", + "optional": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/@braintree/sanitize-url": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", @@ -1168,12 +2021,43 @@ "specificity": "bin/cli.js" } }, + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, "node_modules/@chevrotain/types": { "version": "11.1.2", "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz", "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==", "license": "Apache-2.0" }, + "node_modules/@cloudamqp/amqp-client": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@cloudamqp/amqp-client/-/amqp-client-2.1.1.tgz", + "integrity": "sha512-u4nOBfpBTEiohQ/ThJVLmHx+G7mUt1aTXsLqhGVZAnTnA1AZpq/ja1Lhx7equRuE48AhQKS/UDQpV+7grLEF4Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -1416,6 +2300,18 @@ "node": ">=20" } }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@so-ric/colorspace": "^1.1.6", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "node_modules/@dnd-kit/accessibility": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", @@ -2300,6 +3196,33 @@ } } }, + "node_modules/@fal-ai/client": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@fal-ai/client/-/client-1.10.1.tgz", + "integrity": "sha512-c3AVeH31OioiI2J1BfW8Cryi1DhUYldnY3X35nv6xLMq3fU2NQOo+eYaR5mL2O8MoHHh+HzXdQuIyanIyeq+ug==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@msgpack/msgpack": "^3.0.0-beta2", + "eventsource-parser": "^1.1.2", + "robot3": "^0.4.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@fal-ai/client/node_modules/eventsource-parser": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.1.2.tgz", + "integrity": "sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14.18" + } + }, "node_modules/@floating-ui/core": { "version": "1.7.5", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", @@ -2408,6 +3331,86 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/@googleapis/sheets": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/@googleapis/sheets/-/sheets-13.0.2.tgz", + "integrity": "sha512-b1tBlMcfvNEziM4DZCikLOc9iqSlgCK1e5bMKtNQIADRXr1CQmbkHV3ZBVvTsFsjLErgihqO58Itn/kzCnSZ0A==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "googleapis-common": "^8.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz", + "integrity": "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", + "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/grpc-js/node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader/node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@hapi/address": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", @@ -2484,6 +3487,14 @@ "node": ">=18" } }, + "node_modules/@huggingface/tokenizers": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@huggingface/tokenizers/-/tokenizers-0.1.3.tgz", + "integrity": "sha512-8rF/RRT10u+kn7YuUbUg0OF30K8rjTc78aHpxT+qJ1uWSqxT1MHi8+9ltwYfkFYJzT/oS+qw3JVfHtNMGAdqyA==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, "node_modules/@huggingface/transformers": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-3.5.2.tgz", @@ -2549,6 +3560,45 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@ibm-cloud/watsonx-ai": { + "version": "1.7.15", + "resolved": "https://registry.npmjs.org/@ibm-cloud/watsonx-ai/-/watsonx-ai-1.7.15.tgz", + "integrity": "sha512-JUxFuACcKhDC+shavgyLEFjWiSsBjI+tjIBvLrzcHywh0HqTT0m+whe2kL3isPheBzrduSMfovwypbSuKwCp+g==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "form-data": "^4.0.4", + "ibm-cloud-sdk-core": "^5.4.20" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@ibm-generative-ai/node-sdk": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@ibm-generative-ai/node-sdk/-/node-sdk-3.2.4.tgz", + "integrity": "sha512-HvJSYql3lOPYZcGb23mBw0kcWLlCX+n7EDRgJQxz7gIzx9WafUuDyl1IlTCXGfxolm0EhNIub79u9v7owtks0w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@ai-zen/node-fetch-event-source": "^2.1.2", + "fetch-retry": "^5.0.6", + "http-status-codes": "^2.3.0", + "openapi-fetch": "^0.8.2", + "p-queue-compat": "1.0.225", + "yaml": "^2.3.3" + }, + "peerDependencies": { + "@langchain/core": ">=0.1.0" + }, + "peerDependenciesMeta": { + "@langchain/core": { + "optional": true + } + } + }, "node_modules/@iconify/types": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", @@ -2620,6 +3670,54 @@ "@img/sharp-libvips-darwin-x64": "1.2.4" } }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz", + "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-freebsd-wasm32/node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/sharp-freebsd-wasm32/node_modules/@img/sharp-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz", + "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==", + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.1" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@img/sharp-libvips-darwin-arm64": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", @@ -2975,6 +4073,54 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz", + "integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32/node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32/node_modules/@img/sharp-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz", + "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==", + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.1" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@img/sharp-win32-arm64": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", @@ -3388,7 +4534,6 @@ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, "license": "ISC", - "optional": true, "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -3407,7 +4552,6 @@ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", - "optional": true, "engines": { "node": ">=12" }, @@ -3420,8 +4564,7 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", @@ -3429,7 +4572,6 @@ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -3448,7 +4590,6 @@ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -3537,6 +4678,17 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/@jscpd/badge-reporter": { "version": "4.2.5", "resolved": "https://registry.npmjs.org/@jscpd/badge-reporter/-/badge-reporter-4.2.5.tgz", @@ -3645,6 +4797,202 @@ "spark-md5": "^3.0.2" } }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@libsql/client": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@libsql/client/-/client-0.17.4.tgz", + "integrity": "sha512-lYayFWasDV78A+TjlEhr6ubb3odBV6OHjb+wdp8VQcyWWAEIjuwbCHaraEUS4m4yWoo0BvZo96It4VdzZRmRWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@libsql/core": "^0.17.4", + "@libsql/hrana-client": "^0.10.0", + "js-base64": "^3.7.5", + "libsql": "^0.5.28", + "promise-limit": "^2.7.0" + } + }, + "node_modules/@libsql/core": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@libsql/core/-/core-0.17.4.tgz", + "integrity": "sha512-LqF9gIvnJ38nmAH1y/ChizHqDO/MO1wLgA96XrraulEEbqXxLjleSH92YWTolbuJKgPUmGu4aJk9W3UnAcxLOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-base64": "^3.7.5" + } + }, + "node_modules/@libsql/darwin-arm64": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@libsql/darwin-arm64/-/darwin-arm64-0.5.29.tgz", + "integrity": "sha512-K+2RIB1OGFPYQbfay48GakLhqf3ArcbHqPFu7EZiaUcRgFcdw8RoltsMyvbj5ix2fY0HV3Q3Ioa/ByvQdaSM0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@libsql/darwin-x64": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@libsql/darwin-x64/-/darwin-x64-0.5.29.tgz", + "integrity": "sha512-OtT+KFHsKFy1R5FVadr8FJ2Bb1mghtXTyJkxv0trocq7NuHntSki1eUbxpO5ezJesDvBlqFjnWaYYY516QNLhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@libsql/hrana-client": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@libsql/hrana-client/-/hrana-client-0.10.0.tgz", + "integrity": "sha512-OoA4EMqRAC7kn7V2P6EQqRcpZf2W+AjsNIyCizBg339Tq/aMC7sRnzs3SklderhmQWAqEzvv8A2vhxVmWpkVvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@libsql/isomorphic-ws": "^0.1.5", + "js-base64": "^3.7.5" + } + }, + "node_modules/@libsql/isomorphic-ws": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@libsql/isomorphic-ws/-/isomorphic-ws-0.1.5.tgz", + "integrity": "sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ws": "^8.5.4", + "ws": "^8.13.0" + } + }, + "node_modules/@libsql/linux-arm-gnueabihf": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@libsql/linux-arm-gnueabihf/-/linux-arm-gnueabihf-0.5.29.tgz", + "integrity": "sha512-CD4n4zj7SJTHso4nf5cuMoWoMSS7asn5hHygsDuhRl8jjjCTT3yE+xdUvI4J7zsyb53VO5ISh4cwwOtf6k2UhQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@libsql/linux-arm-musleabihf": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@libsql/linux-arm-musleabihf/-/linux-arm-musleabihf-0.5.29.tgz", + "integrity": "sha512-2Z9qBVpEJV7OeflzIR3+l5yAd4uTOLxklScYTwpZnkm2vDSGlC1PRlueLaufc4EFITkLKXK2MWBpexuNJfMVcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@libsql/linux-arm64-gnu": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@libsql/linux-arm64-gnu/-/linux-arm64-gnu-0.5.29.tgz", + "integrity": "sha512-gURBqaiXIGGwFNEaUj8Ldk7Hps4STtG+31aEidCk5evMMdtsdfL3HPCpvys+ZF/tkOs2MWlRWoSq7SOuCE9k3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@libsql/linux-arm64-musl": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@libsql/linux-arm64-musl/-/linux-arm64-musl-0.5.29.tgz", + "integrity": "sha512-fwgYZ0H8mUkyVqXZHF3mT/92iIh1N94Owi/f66cPVNsk9BdGKq5gVpoKO+7UxaNzuEH1roJp2QEwsCZMvBLpqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@libsql/linux-x64-gnu": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@libsql/linux-x64-gnu/-/linux-x64-gnu-0.5.29.tgz", + "integrity": "sha512-y14V0vY0nmMC6G0pHeJcEarcnGU2H6cm21ZceRkacWHvQAEhAG0latQkCtoS2njFOXiYIg+JYPfAoWKbi82rkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@libsql/linux-x64-musl": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@libsql/linux-x64-musl/-/linux-x64-musl-0.5.29.tgz", + "integrity": "sha512-gquqwA/39tH4pFl+J9n3SOMSymjX+6kZ3kWgY3b94nXFTwac9bnFNMffIomgvlFaC4ArVqMnOZD3nuJ3H3VO1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@libsql/win32-x64-msvc": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@libsql/win32-x64-msvc/-/win32-x64-msvc-0.5.29.tgz", + "integrity": "sha512-4/0CvEdhi6+KjMxMaVbFM2n2Z44escBRoEYpR+gZg64DdetzGnYm8mcNLcoySaDJZNaBd6wz5DNdgRmcI4hXcg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@lobehub/icons": { "version": "5.10.1", "resolved": "https://registry.npmjs.org/@lobehub/icons/-/icons-5.10.1.tgz", @@ -3784,6 +5132,239 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.4.12", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.12.tgz", + "integrity": "sha512-QAfAMwNgnYxZ2C6D1HgeP7Gc4i/uvJRim415PCIL9ptRxWMNbWeLBYb2/9R4pGKny/s1FVu2JA2cxCUBUOggrA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@msgpack/msgpack": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz", + "integrity": "sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA==", + "dev": true, + "license": "ISC", + "optional": true, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@napi-rs/canvas": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.80.tgz", + "integrity": "sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==", + "dev": true, + "license": "MIT", + "optional": true, + "workspaces": [ + "e2e/*" + ], + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@napi-rs/canvas-android-arm64": "0.1.80", + "@napi-rs/canvas-darwin-arm64": "0.1.80", + "@napi-rs/canvas-darwin-x64": "0.1.80", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.80", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.80", + "@napi-rs/canvas-linux-arm64-musl": "0.1.80", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.80", + "@napi-rs/canvas-linux-x64-gnu": "0.1.80", + "@napi-rs/canvas-linux-x64-musl": "0.1.80", + "@napi-rs/canvas-win32-x64-msvc": "0.1.80" + } + }, + "node_modules/@napi-rs/canvas-android-arm64": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.80.tgz", + "integrity": "sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-darwin-arm64": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.80.tgz", + "integrity": "sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-darwin-x64": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.80.tgz", + "integrity": "sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.80.tgz", + "integrity": "sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-gnu": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.80.tgz", + "integrity": "sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-musl": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.80.tgz", + "integrity": "sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.80.tgz", + "integrity": "sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-gnu": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.80.tgz", + "integrity": "sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-musl": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.80.tgz", + "integrity": "sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.80.tgz", + "integrity": "sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", @@ -3803,6 +5384,13 @@ "@emnapi/runtime": "^1.7.1" } }, + "node_modules/@neon-rs/load": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@neon-rs/load/-/load-0.0.4.tgz", + "integrity": "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==", + "dev": true, + "license": "MIT" + }, "node_modules/@next/env": { "version": "16.2.10", "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.10.tgz", @@ -4188,6 +5776,19 @@ "node": ">= 10" } }, + "node_modules/@nodable/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -4838,6 +6439,631 @@ "node": ">=20.0" } }, + "node_modules/@openai/agents": { + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/@openai/agents/-/agents-0.11.8.tgz", + "integrity": "sha512-D4XHF2g+Ub/L9fRJT/xpuiCqHyxiKzZbi0BqQxnso42t+J049O/OSvVzFBcRskF4uPFAvs0TOOB7KBbanCwaYQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@openai/agents-core": "0.11.8", + "@openai/agents-openai": "0.11.8", + "@openai/agents-realtime": "0.11.8", + "debug": "^4.4.0", + "openai": "^6.35.0" + }, + "peerDependencies": { + "zod": "^4.0.0" + } + }, + "node_modules/@openai/agents-core": { + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/@openai/agents-core/-/agents-core-0.11.8.tgz", + "integrity": "sha512-TrE34RXXPoWYv2PjXf5hq3Eq+uvRJMNiY+Q5WBgEPjAg60yt2hya8cS2I8qkO6i25MjNJl37a25X0vL/gs5Wdg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "^4.4.0", + "openai": "^6.35.0" + }, + "optionalDependencies": { + "@modelcontextprotocol/sdk": "^1.26.0" + }, + "peerDependencies": { + "zod": "^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@openai/agents-openai": { + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/@openai/agents-openai/-/agents-openai-0.11.8.tgz", + "integrity": "sha512-XjHCnJPGapgZBlh8y5oxU7zV0hrAQTF5im6HpUwaPcH5CeRFLtc06VXLso0vJ5G3g9e/J5gIh3S1iAxiJqEAVQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@openai/agents-core": "0.11.8", + "debug": "^4.4.0", + "openai": "^6.35.0" + }, + "peerDependencies": { + "zod": "^4.0.0" + } + }, + "node_modules/@openai/agents-realtime": { + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/@openai/agents-realtime/-/agents-realtime-0.11.8.tgz", + "integrity": "sha512-i1qEGUE8GTW0neWgAc1aj/3wZFtstz8bVG2BvVbU/BzQbyhZV8j3CvndkMJGFfgeobvVmn2qGTV5Ry6ibfuxeQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@openai/agents-core": "0.11.8", + "@types/ws": "^8.18.1", + "debug": "^4.4.0", + "ws": "^8.18.1" + }, + "peerDependencies": { + "zod": "^4.0.0" + } + }, + "node_modules/@openai/codex": { + "version": "0.142.5", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.5.tgz", + "integrity": "sha512-WQEpD7l3k68eIAP0aq28EdR18ENBAf8DyprzFhzNwCOQJSv4nHzpwT8Fl30IJacprko2ZCmUBZjM2u941l2yLw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "bin": { + "codex": "bin/codex.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@openai/codex-darwin-arm64": "npm:@openai/codex@0.142.5-darwin-arm64", + "@openai/codex-darwin-x64": "npm:@openai/codex@0.142.5-darwin-x64", + "@openai/codex-linux-arm64": "npm:@openai/codex@0.142.5-linux-arm64", + "@openai/codex-linux-x64": "npm:@openai/codex@0.142.5-linux-x64", + "@openai/codex-win32-arm64": "npm:@openai/codex@0.142.5-win32-arm64", + "@openai/codex-win32-x64": "npm:@openai/codex@0.142.5-win32-x64" + } + }, + "node_modules/@openai/codex-darwin-arm64": { + "name": "@openai/codex", + "version": "0.142.5-darwin-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.5-darwin-arm64.tgz", + "integrity": "sha512-l43p8xv+Z/2/b6fCUc7/FmcQZsaPB7RFizLponGwHAnFOWe3i9Vky69p+up3BUam9AetoQQUv7Mo+2KdaFEqhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@openai/codex-darwin-x64": { + "name": "@openai/codex", + "version": "0.142.5-darwin-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.5-darwin-x64.tgz", + "integrity": "sha512-yk6A06/VmW7NFsa48OVPaj//g/zeSpd79wjuqfXZwW8ZKRYQm3+wCd3hWjPl79F3QnXvDvM2j3JMIBL3m3GXXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@openai/codex-linux-arm64": { + "name": "@openai/codex", + "version": "0.142.5-linux-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.5-linux-arm64.tgz", + "integrity": "sha512-77ka5PSnm5HdxdBT99IwntCasmbqevlS0eiC0AtEb6ZXCLkim2gm0AWm+jNYy0EhbssvNK+KghayWo34HMgXeA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@openai/codex-linux-x64": { + "name": "@openai/codex", + "version": "0.142.5-linux-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.5-linux-x64.tgz", + "integrity": "sha512-pxY+d3NgNE57Y/MApD3/TZUAygxJN6I9h3ZeDUwe67mxWjUxsuapxMRFTKSznCalYbRAeZp752+AAXmUbmguEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@openai/codex-sdk": { + "version": "0.142.5", + "resolved": "https://registry.npmjs.org/@openai/codex-sdk/-/codex-sdk-0.142.5.tgz", + "integrity": "sha512-MConZ+eoBoZmkc4reezuzOgLtoI1BQBzo/nVYsSjtAIBpwKcgeEm1rfmqfUnTfFaBNHFTxBntcS7ZeQYuDPbWA==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@openai/codex": "0.142.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@openai/codex-win32-arm64": { + "name": "@openai/codex", + "version": "0.142.5-win32-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.5-win32-arm64.tgz", + "integrity": "sha512-65BEqGbUZ7r0ayunIHdBjo5crwgbwKX/6puOcO+VCswUw/dXvDsN2IGcbXB52+bS9U5+FxP783cUHfTT6m40DQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@openai/codex-win32-x64": { + "name": "@openai/codex", + "version": "0.142.5-win32-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.5-win32-x64.tgz", + "integrity": "sha512-a+wI4PEx9a2fg6V5ueTTDkOkr1XpEvA5RFXIbo/L2hOfzMmGtyRnbG24bCGu5Q2RSgVxSQV0aLkdb3vdYMNH9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@opencode-ai/sdk": { + "version": "1.17.20", + "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.17.20.tgz", + "integrity": "sha512-Urb7Tp4mvJmyckNI/N79uEpWzZOyn4lE5LVPDVidXZ8BIlw6kxx2ctTPRYPJDF8HLPWGCZ7pCpHcmWgQCYyiNQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "cross-spawn": "7.0.6" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.219.0.tgz", + "integrity": "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.9.0.tgz", + "integrity": "sha512-OQ0vzvbZBiUhjqLnUaoNfYmP8553Crr3aggB4y0ZUi815mZ7idpdJXQmoKdeBKJelYttoBlLSSHubmyw3wvX4w==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http": { + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.219.0.tgz", + "integrity": "sha512-9t6SvBXXBEjOBcIzgozvBbd3jWrv3Gt3ngGhl1fhdZ/zRc7oZDVOFEqbi2zlBpW9BXhgDMKv422J0DL/3iQWfw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.8.0", + "@opentelemetry/otlp-exporter-base": "0.219.0", + "@opentelemetry/otlp-transformer": "0.219.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/sdk-trace-base": "2.8.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/resources": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", + "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.8.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.8.0.tgz", + "integrity": "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.219.0.tgz", + "integrity": "sha512-zvIxQX/AZUVKDU+hCuYx+7UkiP7GRdnk1ZbFQRYzHvYp47cAWR4j3IhoPhV9KaeXEv2xdGq3IA6PnpzDmLcmSA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.8.0", + "@opentelemetry/otlp-transformer": "0.219.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.219.0.tgz", + "integrity": "sha512-aaYKAyXhw9VchKZVGOopD3Gw/kPsyrX2c6IQ0AW32mTjqmZOh5Y6Gf5OYqTNqVktAeBjmFinhyFaCwW6GYK9YQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.219.0", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/sdk-logs": "0.219.0", + "@opentelemetry/sdk-metrics": "2.8.0", + "@opentelemetry/sdk-trace-base": "2.8.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", + "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.8.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.8.0.tgz", + "integrity": "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.219.0.tgz", + "integrity": "sha512-s6lTKRakaPClvKoWHRChxnXjDMkM/TQ30ff78jN6EBGf7MI7VzANE5PU3f4z9qDUudWjvZjOLHG0rBnBKYvoXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.219.0", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", + "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.8.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.8.0.tgz", + "integrity": "sha512-UDBGaj6W0Rgy5rTTaoxs8gVGF/aGkAKyjurJv7se6wjRxJu7FoquTLT/vt54DZfo4crbprYfhX/SOK9+BPw1qg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", + "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.8.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.9.0.tgz", + "integrity": "sha512-sGA19HvtrrSKYsseHphluH6j3p6Xa3fqc7c7y8f/7mYWejc1lyDFcpSdD1kYa50HCLUeEo4zA5bW0pniaPszuw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.9.0.tgz", + "integrity": "sha512-cp9zmTl62R8PJrpvFcmc8N2JQU/xfa0S+61q511Nji+QxCfZ8Ifvg7H27G8cANe4crg4RTrWsVvanHiXjSp6ag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/sdk-trace": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.9.0.tgz", + "integrity": "sha512-ec9a7ps37huy5itYk0MalaZdSLlM6AXWp/FhtEjgMpp5leEGojBDvAl/UWttQnkMZOvFHKzRESn8TD3yKTF5nQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/context-async-hooks": "2.9.0", + "@opentelemetry/core": "2.9.0", + "@opentelemetry/sdk-trace-base": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace/node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz", + "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/@orama/orama": { "version": "3.1.18", "resolved": "https://registry.npmjs.org/@orama/orama/-/orama-3.1.18.tgz", @@ -6266,6 +8492,21 @@ "node": ">=14" } }, + "node_modules/@playwright/browser-chromium": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/@playwright/browser-chromium/-/browser-chromium-1.61.1.tgz", + "integrity": "sha512-t3/zE0i9gik5R/NpRs7G2Xo/6NPeABW6ReplGdtkeWeAkaV764CgFgoKjJo21D2xgjnvDvRYubqBUu4xl0VCqA==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "playwright-core": "1.61.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@playwright/test": { "version": "1.61.1", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz", @@ -6323,40 +8564,50 @@ "node": ">=12" } }, + "node_modules/@posthog/core": { + "version": "1.23.1", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.23.1.tgz", + "integrity": "sha512-GViD5mOv/mcbZcyzz3z9CS0R79JzxVaqEz4sP5Dsea178M/j3ZWe6gaHDZB9yuyGfcmIMQ/8K14yv+7QrK4sQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.6" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "license": "BSD-3-Clause", - "optional": true + "devOptional": true, + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/base64": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "license": "BSD-3-Clause", - "optional": true + "devOptional": true, + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/codegen": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", - "license": "BSD-3-Clause", - "optional": true + "devOptional": true, + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", - "license": "BSD-3-Clause", - "optional": true + "devOptional": true, + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "devOptional": true, "license": "BSD-3-Clause", - "optional": true, "dependencies": { "@protobufjs/aspromise": "^1.1.1" } @@ -6365,29 +8616,29 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause", - "optional": true + "devOptional": true, + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause", - "optional": true + "devOptional": true, + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/pool": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause", - "optional": true + "devOptional": true, + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/utf8": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", - "license": "BSD-3-Clause", - "optional": true + "devOptional": true, + "license": "BSD-3-Clause" }, "node_modules/@radix-ui/number": { "version": "1.1.2", @@ -7146,6 +9397,99 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, + "node_modules/@redis/bloom": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.12.1.tgz", + "integrity": "sha512-PUUfv+ms7jgPSBVoo/DN4AkPHj4D5TZSd6SbJX7egzBplkYUcKmHRE8RKia7UtZ8bSQbLguLvxVO+asKtQfZWA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 18.19.0" + }, + "peerDependencies": { + "@redis/client": "^5.12.1" + } + }, + "node_modules/@redis/client": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.12.1.tgz", + "integrity": "sha512-7aPGWeqA3uFm43o19umzdl16CEjK/JQGtSXVPevplTaOU3VJA/rseBC1QvYUz9lLDIMBimc4SW/zrW4S89BaCA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "cluster-key-slot": "1.1.2" + }, + "engines": { + "node": ">= 18.19.0" + }, + "peerDependencies": { + "@node-rs/xxhash": "^1.1.0", + "@opentelemetry/api": ">=1 <2" + }, + "peerDependenciesMeta": { + "@node-rs/xxhash": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + } + } + }, + "node_modules/@redis/client/node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@redis/json": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.12.1.tgz", + "integrity": "sha512-eOze75esLve4vfqDel7aMX08CNaiLLQS2fV8mpRN9NxPe1rVR4vQyYiW/OgtGUysF6QOr9ANhfxABKNOJfXdKg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 18.19.0" + }, + "peerDependencies": { + "@redis/client": "^5.12.1" + } + }, + "node_modules/@redis/search": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.12.1.tgz", + "integrity": "sha512-ItlxbxC9cKI6IU1TLWoczwJCRb6TdmkEpWv05UrPawqaAnWGRu3rcIqsc5vN483T2fSociuyV1UkWIL5I4//2w==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 18.19.0" + }, + "peerDependencies": { + "@redis/client": "^5.12.1" + } + }, + "node_modules/@redis/time-series": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.12.1.tgz", + "integrity": "sha512-c6JL6E3EcZJuNqKFz+KM+l9l5mpcQiKvTwgA3blt5glWJ8hjDk0yeHN3beE/MpqYIQ8UEX44ItQzgkE/gCBELQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 18.19.0" + }, + "peerDependencies": { + "@redis/client": "^5.12.1" + } + }, "node_modules/@reduxjs/toolkit": { "version": "2.11.2", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", @@ -7436,6 +9780,23 @@ "dev": true, "license": "MIT" }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -7636,6 +9997,23 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/@simple-git/args-pathspec": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@simple-git/args-pathspec/-/args-pathspec-1.0.3.tgz", + "integrity": "sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@simple-git/argv-parser": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@simple-git/argv-parser/-/argv-parser-1.1.1.tgz", + "integrity": "sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@simple-git/args-pathspec": "^1.0.3" + } + }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", @@ -7674,13 +10052,77 @@ "size-limit": "12.1.0" } }, + "node_modules/@slack/logger": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@slack/logger/-/logger-4.0.1.tgz", + "integrity": "sha512-6cmdPrV/RYfd2U0mDGiMK8S7OJqpCTm7enMLRR3edccsPX8j7zXTLnaEF4fhxxJJTAIOil6+qZrnUPTuaLvwrQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": ">=18" + }, + "engines": { + "node": ">= 18", + "npm": ">= 8.6.0" + } + }, + "node_modules/@slack/types": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/@slack/types/-/types-2.22.0.tgz", + "integrity": "sha512-sZ9lIgJhPX2qft/tKWiklFlc0o1FWeI7QtciZJfW1+ErH1eGGHvOZ8e73sleTCFEFJp1q/R0WeS8Oa7AsiDprg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12.13.0", + "npm": ">= 6.12.0" + } + }, + "node_modules/@slack/web-api": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@slack/web-api/-/web-api-7.19.0.tgz", + "integrity": "sha512-ItjyjEZml+LDH8CjcCLRLJHh7VZtevPKExrRN3l5KWyBliyDnGAeoO4Y+K+fFBmRpKLYVPgqWMX4THldv2HVtA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@slack/logger": "^4.0.1", + "@slack/types": "^2.21.0", + "@types/node": ">=18", + "@types/retry": "0.12.0", + "axios": "^1.16.0", + "eventemitter3": "^5.0.1", + "form-data": "^4.0.4", + "is-electron": "2.2.2", + "is-stream": "^2", + "p-queue": "^6", + "p-retry": "^4", + "retry": "^0.13.1" + }, + "engines": { + "node": ">= 18", + "npm": ">= 8.6.0" + } + }, + "node_modules/@slack/web-api/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/@smithy/core": { - "version": "3.29.1", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.29.1.tgz", - "integrity": "sha512-qoiY4nrk5OCu1+eIR1VB8l5DmON/oKiqrd5zZFAhXJXjJlLWQusKEW/SkBDAtGDcPaz86m9kfcE1lngU0GlM6A==", + "version": "3.29.3", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.29.3.tgz", + "integrity": "sha512-L+Ys6ecjk5vwPMAKHBpPKlJ3DkqwNcnfEISXBZIsVvWG/XKXfsAP8mwIYlTeLcd2ElHdesPI8OuOmJSFAPhm6A==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.15.1", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -7688,13 +10130,13 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.4.6.tgz", - "integrity": "sha512-B2WQ/PV/H6Jeg3lrIq6bKUfa6Hy01mtK7CGs6lhjzHA6k4aagldH6T6eEjnzKl4HI0cJnAsxfJ19pgb5PV+CVQ==", + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.4.8.tgz", + "integrity": "sha512-q9J7JTiXrAhB8sDp4px97uEPT7CwKH61Co78grdNQvU8QZAdiuaSRhP0tUVf2ogy36RZTrlMU1rBmDEH+cnkiA==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.29.1", - "@smithy/types": "^4.15.1", + "@smithy/core": "^3.29.3", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -7702,13 +10144,13 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.6.3.tgz", - "integrity": "sha512-CwCc/7SMTj45y97MUnDTbTaxvtAsiNNRm81z3abROIuMbMsC2Iy5EKfkkVdsKrz8WExQAAMx1EJapq+9j4fFTQ==", + "version": "5.6.5", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.6.5.tgz", + "integrity": "sha512-SuqeisTyPoiIPtIYru/sGxGyXzmZ+8nnFOhC+qRPglt06Ebd1yH//CDltZB2J/3WBNVhwfUaZ0EtHB3cm2X32g==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.29.1", - "@smithy/types": "^4.15.1", + "@smithy/core": "^3.29.3", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -7716,13 +10158,13 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.9.3", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.9.3.tgz", - "integrity": "sha512-qZTa4gQFUo8RM02rk6q5UVTDLNrQ1oS20LsepBzqq1QBVc/EHJ03OOUADcqMZiXHArW+Y7+OGY0BpdTwZRq/Yg==", + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.9.5.tgz", + "integrity": "sha512-bNqdxTQTxmLbomSmlkZFz8L6B/feQ2HHzw4L2zY7Ecp2XffYAZq2uzdWDdxJHJFbEvqd+SRuluJso0P8+xPdbw==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.29.1", - "@smithy/types": "^4.15.1", + "@smithy/core": "^3.29.3", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -7730,13 +10172,13 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.6.2.tgz", - "integrity": "sha512-QgHflghMoPxCJ9axiCVh8KZfbC9fuP6vkXXyK//E3cq7nLaSSyyLj0GAoqVWezYeDQmXIZhmlRvLE16jsqDK6g==", + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.6.4.tgz", + "integrity": "sha512-B89bpf2t/y/wia6LZ+4JfHXYQT9PnVftsH05rgJKKIStS7r/4XSs9HOjtPoLtgcA6HCW9jVqX5DBbq7E0PAkiQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.29.1", - "@smithy/types": "^4.15.1", + "@smithy/core": "^3.29.3", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -7744,9 +10186,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.15.1", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.15.1.tgz", - "integrity": "sha512-x3L0XSACF6UYzKpa9biqiRMgvH5+wnFFew9Tm/grFYqgaupPwx/+ojDPpPJM8dZON3S9tjz5U+PQYsCBd1Mw5Q==", + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.16.1.tgz", + "integrity": "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -7755,6 +10197,31 @@ "node": ">=18.0.0" } }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -8074,14 +10541,14 @@ "license": "Apache-2.0" }, "node_modules/@swc/core": { - "version": "1.15.21", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.21.tgz", - "integrity": "sha512-fkk7NJcBscrR3/F8jiqlMptRHP650NxqDnspBMrRe5d8xOoCy9MLL5kOBLFXjFLfMo3KQQHhk+/jUULOMlR1uQ==", + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.43.tgz", + "integrity": "sha512-1CuKjFkPxIgGdeHVuNbkxmBxkcbdc08u0aiI43pFq6yY1tTVKmXT9hFEooyyKs/sJ3xf1GPHyEwTtk9Xl8dvQw==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.25" + "@swc/types": "^0.1.27" }, "engines": { "node": ">=10" @@ -8091,18 +10558,18 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.15.21", - "@swc/core-darwin-x64": "1.15.21", - "@swc/core-linux-arm-gnueabihf": "1.15.21", - "@swc/core-linux-arm64-gnu": "1.15.21", - "@swc/core-linux-arm64-musl": "1.15.21", - "@swc/core-linux-ppc64-gnu": "1.15.21", - "@swc/core-linux-s390x-gnu": "1.15.21", - "@swc/core-linux-x64-gnu": "1.15.21", - "@swc/core-linux-x64-musl": "1.15.21", - "@swc/core-win32-arm64-msvc": "1.15.21", - "@swc/core-win32-ia32-msvc": "1.15.21", - "@swc/core-win32-x64-msvc": "1.15.21" + "@swc/core-darwin-arm64": "1.15.43", + "@swc/core-darwin-x64": "1.15.43", + "@swc/core-linux-arm-gnueabihf": "1.15.43", + "@swc/core-linux-arm64-gnu": "1.15.43", + "@swc/core-linux-arm64-musl": "1.15.43", + "@swc/core-linux-ppc64-gnu": "1.15.43", + "@swc/core-linux-s390x-gnu": "1.15.43", + "@swc/core-linux-x64-gnu": "1.15.43", + "@swc/core-linux-x64-musl": "1.15.43", + "@swc/core-win32-arm64-msvc": "1.15.43", + "@swc/core-win32-ia32-msvc": "1.15.43", + "@swc/core-win32-x64-msvc": "1.15.43" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" @@ -8114,9 +10581,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.15.21", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.21.tgz", - "integrity": "sha512-SA8SFg9dp0qKRH8goWsax6bptFE2EdmPf2YRAQW9WoHGf3XKM1bX0nd5UdwxmC5hXsBUZAYf7xSciCler6/oyA==", + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.43.tgz", + "integrity": "sha512-v1aVuvXdo/BHxJzco9V2xpHrvwWmhfS8t6gziY5wJxd+Z2h8AeJRnAwPD8itCDaGXVBwJ/CaKfxEzTkG0Va0OA==", "cpu": [ "arm64" ], @@ -8130,9 +10597,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.15.21", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.21.tgz", - "integrity": "sha512-//fOVntgowz9+V90lVsNCtyyrtbHp3jWH6Rch7MXHXbcvbLmbCTmssl5DeedUWLLGiAAW1wksBdqdGYOTjaNLw==", + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.43.tgz", + "integrity": "sha512-lp3d4Lamc8dt5huYdGLSR+9hLxmfr1jb0l+4XXG2zPqZwYWRN9R0U2qYoTrggiU2RWW0oV9VbWM3kBnqIc2kdQ==", "cpu": [ "x64" ], @@ -8146,9 +10613,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.15.21", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.21.tgz", - "integrity": "sha512-meNI4Sh6h9h8DvIfEc0l5URabYMSuNvyisLmG6vnoYAS43s8ON3NJR8sDHvdP7NJTrLe0q/x2XCn6yL/BeHcZg==", + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.43.tgz", + "integrity": "sha512-JWTQQELtsG5GgphDrr/XqqmM2pDN3cZqbMS0Mrg+iTiXL3F74sn/S2IyYE/5u4h2KLkTf9qQ7dXyxsbx7YzkeA==", "cpu": [ "arm" ], @@ -8162,12 +10629,15 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.15.21", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.21.tgz", - "integrity": "sha512-QrXlNQnHeXqU2EzLlnsPoWEh8/GtNJLvfMiPsDhk+ht6Xv8+vhvZ5YZ/BokNWSIZiWPKLAqR0M7T92YF5tmD3g==", + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.43.tgz", + "integrity": "sha512-B4otJRdPWIsmiSBf0uG7Z/+vMWmkufjz5MmYxubwKuZazDW14Zd3symga1N62QR4RT+kEFeHEgsXfZGyn/w0hw==", "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -8178,12 +10648,15 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.15.21", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.21.tgz", - "integrity": "sha512-8/yGCMO333ultDaMQivE5CjO6oXDPeeg1IV4sphojPkb0Pv0i6zvcRIkgp60xDB+UxLr6VgHgt+BBgqS959E9g==", + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.43.tgz", + "integrity": "sha512-6zB6OnpViBxYy4tgY3v2i6AZY9fwkcHZ032UOwtwUuW1d19sdT07qF0kZe6/3UR1tUaK6jjg2rmVcUIBCEYVjQ==", "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -8194,12 +10667,15 @@ } }, "node_modules/@swc/core-linux-ppc64-gnu": { - "version": "1.15.21", - "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.21.tgz", - "integrity": "sha512-ucW0HzPx0s1dgRvcvuLSPSA/2Kk/VYTv9st8qe1Kc22Gu0Q0rH9+6TcBTmMuNIp0Xs4BPr1uBttmbO1wEGI49Q==", + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.43.tgz", + "integrity": "sha512-coxE1ZWdB3uSDVNoEtYNrRi/1epvckZx9cTJ8ICUxTMTxGk+yvQ/Twacp3ruZSaMPGCriUjP86C37VhaT6nyRg==", "cpu": [ "ppc64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -8210,12 +10686,15 @@ } }, "node_modules/@swc/core-linux-s390x-gnu": { - "version": "1.15.21", - "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.21.tgz", - "integrity": "sha512-ulTnOGc5I7YRObE/9NreAhQg94QkiR5qNhhcUZ1iFAYjzg/JGAi1ch+s/Ixe61pMIr8bfVrF0NOaB0f8wjaAfA==", + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.43.tgz", + "integrity": "sha512-lXfLhs+LpBsD5inuYx+YDH5WsPPBQ95KPUiy8P5wq9ob9xKDZFqwNfU2QW6bGO8NqRO/H9JQomTSt5Yyh+FGfA==", "cpu": [ "s390x" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -8226,12 +10705,15 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.15.21", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.21.tgz", - "integrity": "sha512-D0RokxtM+cPvSqJIKR6uja4hbD+scI9ezo95mBhfSyLUs9wnPPl26sLp1ZPR/EXRdYm3F3S6RUtVi+8QXhT24Q==", + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.43.tgz", + "integrity": "sha512-07XnKwTmKy8TGOZG3D9fRnLWGynxPjwQnZLVmBFbo6F+7vHYzBIOuwXEhemrChBWb6yDNZsVCcMWCPX6FDD2xg==", "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -8242,12 +10724,15 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.15.21", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.21.tgz", - "integrity": "sha512-nER8u7VeRfmU6fMDzl1NQAbbB/G7O2avmvCOwIul1uGkZ2/acbPH+DCL9h5+0yd/coNcxMBTL6NGepIew+7C2w==", + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.43.tgz", + "integrity": "sha512-TJc+bsSIaBh+hZvZ5GRtW/K1bw66TJ9vsUwvVIsZdiWxU5ObLwZvfcnZ3UpgVfMnFibRes9uriJrQNBHEEogRQ==", "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -8258,9 +10743,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.15.21", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.21.tgz", - "integrity": "sha512-+/AgNBnjYugUA8C0Do4YzymgvnGbztv7j8HKSQLvR/DQgZPoXQ2B3PqB2mTtGh/X5DhlJWiqnunN35JUgWcAeQ==", + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.43.tgz", + "integrity": "sha512-jfd7s2/bUQYkOHLs+LWQNKZdmDa8+sufKLllhpWAhVQ2GDCwsHe3vR/j+OSiItZNtkzFuaawa3+SAKz9y5gYfw==", "cpu": [ "arm64" ], @@ -8274,9 +10759,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.15.21", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.21.tgz", - "integrity": "sha512-IkSZj8PX/N4HcaFhMQtzmkV8YSnuNoJ0E6OvMwFiOfejPhiKXvl7CdDsn1f4/emYEIDO3fpgZW9DTaCRMDxaDA==", + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.43.tgz", + "integrity": "sha512-rLAE8JvucqEW1ZGohxPQrQWPBQeJG4+ypKbWfdlU/qmKScvCkxf9/Jxnzki1dkUQCQ7P5Enp13RlvqOlvx/32g==", "cpu": [ "ia32" ], @@ -8290,9 +10775,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.15.21", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.21.tgz", - "integrity": "sha512-zUyWso7OOENB6e1N1hNuNn8vbvLsTdKQ5WKLgt/JcBNfJhKy/6jmBmqI3GXk/MyvQKd5SLvP7A0F36p7TeDqvw==", + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.43.tgz", + "integrity": "sha512-h8MLDHZcfIukwQWj03rIJZx1I0E81AYj2X7J/nGErG4nz+QAv6G1Z+peotvinL3lqpbo32tLYSMFo32/ySzxKg==", "cpu": [ "x64" ], @@ -8321,14 +10806,27 @@ } }, "node_modules/@swc/types": { - "version": "0.1.26", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.26.tgz", - "integrity": "sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==", + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.27.tgz", + "integrity": "sha512-K6h3iUlqeM946U4sXFYeahefR1YBbXJvko+hv8WS8/0BNJ4OHiHRywMnQUJCqkR7Y9+hqQ1TvEpiKqUhz7NEFg==", "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3" } }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@tailwindcss/node": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz", @@ -8956,6 +11454,33 @@ } } }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/@toon-format/toon": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@toon-format/toon/-/toon-2.3.0.tgz", @@ -9056,6 +11581,19 @@ "bun-types": "1.3.14" } }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -9067,6 +11605,16 @@ "assertion-error": "^2.0.1" } }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/d3": { "version": "7.4.3", "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", @@ -9366,6 +11914,13 @@ "@types/unist": "*" } }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -9387,6 +11942,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/long": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", @@ -9449,6 +12014,14 @@ "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", "license": "MIT" }, + "node_modules/@types/pegjs": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@types/pegjs/-/pegjs-0.10.6.tgz", + "integrity": "sha512-eLYXDbZWXh2uxf+w8sXS8d6KSoXTswfps6fvCUuVAGN8eRpfe7h9eSRydxiSJvo9Bf+GzifsDOr9TMQlmJdmkw==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/@types/react": { "version": "19.2.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", @@ -9469,6 +12042,34 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/readable-stream": { + "version": "4.0.24", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.24.tgz", + "integrity": "sha512-NRvUNC/JFGPJvqdAfEve8oginbM6V08u5NzLWpG8MwA2kTPOLnqk+wpwuPT+mp3aUsxyuT6m2gnrPuHYCruzEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/@types/safe-regex": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@types/safe-regex/-/safe-regex-1.1.6.tgz", @@ -9490,6 +12091,21 @@ "license": "MIT", "optional": true }, + "node_modules/@types/tough-cookie": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz", + "integrity": "sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -9509,6 +12125,25 @@ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", "license": "MIT" }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@types/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/webidl-conversions": "*" + } + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -9801,6 +12436,48 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.7.tgz", + "integrity": "sha512-JVUD8X2tfDMWjcjLs4yVxxVrS8yR5vnh386GAXT9Qj79nBxxXSaHFQZg5FweLmT8HlPQ3kii6noUB+Z9RN7DvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@typespec/ts-http-runtime/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@typespec/ts-http-runtime/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -10099,6 +12776,16 @@ "d3-transition": "^3.0.1" } }, + "node_modules/@vercel/oidc": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.2.0.tgz", + "integrity": "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 20" + } + }, "node_modules/@vitejs/plugin-react": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz", @@ -10252,6 +12939,16 @@ "license": "BSD-3-Clause", "optional": true }, + "node_modules/@xmldom/xmldom": { + "version": "0.9.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz", + "integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.6" + } + }, "node_modules/@xyflow/react": { "version": "12.11.2", "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.11.2.tgz", @@ -10359,6 +13056,13 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/a-sync-waterfall": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", + "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==", + "dev": true, + "license": "MIT" + }, "node_modules/abbrev": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", @@ -10369,6 +13073,19 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -10403,6 +13120,41 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/adm-zip": { + "version": "0.5.18", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.18.tgz", + "integrity": "sha512-ufJnssQGbxzLNS1Ho9bCtX4rQKCCvoVuDLHoJyc3F9dOGDB4BkWs2Ci0kv53lqocAEQ/Cbi+I2XCsNYGqVYqng==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0" + } + }, + "node_modules/afinn-165": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/afinn-165/-/afinn-165-2.0.2.tgz", + "integrity": "sha512-mJ/RLUfpXfQA6bzugv+bBsc/QYkVrKaLYeS8fWBpKbTCsonv4iuV9ET0fgReEunm9vKLkaNgnekuSNlTC3WQ1Q==", + "dev": true, + "license": "MIT", + "optional": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/afinn-165-financialmarketnews": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/afinn-165-financialmarketnews/-/afinn-165-financialmarketnews-3.0.0.tgz", + "integrity": "sha512-0g9A1S3ZomFIGDTzZ0t6xmv4AuokBvBmpes8htiyHpH7N4xDmvSQL6UxL/Zcs2ypRb3VwgCscaD8Q3zEawKYhw==", + "dev": true, + "license": "MIT", + "optional": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/agent-base": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", @@ -10412,6 +13164,25 @@ "node": ">= 20" } }, + "node_modules/ai": { + "version": "6.0.225", + "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.225.tgz", + "integrity": "sha512-TCkt/NxyZFyXyHeGO/4FMsdTIoCdwV/e78jG9iEHIjnwrAHYlqOwVj7WDFsNRqXUeoUefgbiOkNuE5k8jdpFRg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/gateway": "3.0.149", + "@ai-sdk/provider": "3.0.14", + "@ai-sdk/provider-utils": "4.0.38", + "@opentelemetry/api": "^1.9.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, "node_modules/ajv": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", @@ -10587,6 +13358,33 @@ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", "license": "MIT" }, + "node_modules/anynum": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", + "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/apparatus": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/apparatus/-/apparatus-0.0.10.tgz", + "integrity": "sha512-KLy/ugo33KZA7nugtQ7O0E1c8kQ52N3IvD/XgIh4w/Nr28ypfkwDfA67F1ev4N1m5D+BOk1+b2dEJDfpj/VvZg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "sylvester": ">= 0.0.8" + }, + "engines": { + "node": ">=0.2.6" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -10792,6 +13590,17 @@ "dev": true, "license": "MIT" }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, "node_modules/asn1js": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.7.tgz", @@ -10823,6 +13632,19 @@ "node": ">=12" } }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -10839,6 +13661,13 @@ "astring": "bin/astring" } }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -10902,6 +13731,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "dev": true, + "license": "MIT" + }, "node_modules/axe-core": { "version": "4.12.1", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.12.1.tgz", @@ -11015,6 +13851,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "devOptional": true, "funding": [ { "type": "github", @@ -11029,8 +13866,17 @@ "url": "https://feross.org/support" } ], + "license": "MIT" + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true, "license": "MIT", - "optional": true + "engines": { + "node": "^4.5.0 || >= 5.9" + } }, "node_modules/baseline-browser-mapping": { "version": "2.10.13", @@ -11044,6 +13890,16 @@ "node": ">=6.0.0" } }, + "node_modules/basic-ftp": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/bcryptjs": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", @@ -11078,6 +13934,17 @@ "require-from-string": "^2.0.2" } }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "dev": true, + "license": "Unlicense", + "optional": true, + "engines": { + "node": ">=0.6" + } + }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -11088,6 +13955,16 @@ "node": "*" } }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/bin-links": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/bin-links/-/bin-links-6.0.2.tgz", @@ -11105,6 +13982,35 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/binary-extensions": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-3.1.0.tgz", + "integrity": "sha512-Jvvd9hy1w+xUad8+ckQsWA/V1AoyubOvqn0aygjMOVM4BfIaRav1NFS3LsTSDaV4n4FtcCtQXvzep1E6MboqwQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/binaryextensions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-6.11.0.tgz", + "integrity": "sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "editions": "^6.21.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, "node_modules/bindings": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", @@ -11288,6 +14194,19 @@ "node": ">=8" } }, + "node_modules/broker-factory": { + "version": "3.1.15", + "resolved": "https://registry.npmjs.org/broker-factory/-/broker-factory-3.1.15.tgz", + "integrity": "sha512-ko+aWvgNuP49meGrdjUu7rC+Y+Wai3cCPxP3xWwHsHfehFjOh5ZQM2yC4gEB2UddeZ/YXhm0K1eG/L6fxym2Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "fast-unique-numbers": "^9.0.27", + "tslib": "^2.8.1", + "worker-factory": "^7.0.50" + } + }, "node_modules/browserslist": { "version": "4.28.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", @@ -11322,6 +14241,17 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bson": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/bson/-/bson-7.3.1.tgz", + "integrity": "sha512-h/C0qe6857pQhcSJHLfsR1uYGj98Ge3wKAD3Ed9KqH3wcVh+BM4Jq4xISD7vs9OPuT07n+q3QQVjslJ286j6ag==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -11356,6 +14286,21 @@ "node": ">=8.0.0" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, "node_modules/builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", @@ -11529,6 +14474,56 @@ "node": "20 || >=22" } }, + "node_modules/cache-manager": { + "version": "7.2.9", + "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-7.2.9.tgz", + "integrity": "sha512-d4vceEyYe95gPxEyQchlEOH9vJlkNRW8G6gzFzzMTxJK9PahYMhC9chrEqgZN0HulROjgw3IzmWVNk7Q7ytiGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.5.0", + "keyv": "^5.6.0" + } + }, + "node_modules/cache-manager/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -11732,6 +14727,17 @@ "dev": true, "license": "MIT" }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": "*" + } + }, "node_modules/chokidar": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", @@ -11922,6 +14928,64 @@ "node": ">=10" } }, + "node_modules/cli-progress": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", + "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.3" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cli-progress/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-progress/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-progress/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-progress/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/cli-spinners": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", @@ -12023,6 +15087,155 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/clipboardy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-4.0.0.tgz", + "integrity": "sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^8.0.1", + "is-wsl": "^3.1.0", + "is64bit": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clipboardy/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/clipboardy/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clipboardy/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/clipboardy/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clipboardy/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clipboardy/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clipboardy/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clipboardy/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clipboardy/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -12101,6 +15314,29 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clone-response/node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -12160,6 +15396,20 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/color": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", + "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^3.1.3", + "color-string": "^2.1.3" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -12178,6 +15428,52 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/color-string": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", + "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-string/node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", + "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", @@ -12225,6 +15521,13 @@ "node": ">=22.12.0" } }, + "node_modules/commist": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz", + "integrity": "sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==", + "dev": true, + "license": "MIT" + }, "node_modules/common-ancestor-path": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-2.0.0.tgz", @@ -12235,6 +15538,79 @@ "node": ">= 18" } }, + "node_modules/complex.js": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.4.3.tgz", + "integrity": "sha512-UrQVSUur14tNX6tiP4y8T4w4FeJAX3bi2cIv0pu/DTLFNxoq7z2Yh83Vfzztj6Px3X/lubqQ9IrPp7Bpn6p4MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/compute-scroll-into-view": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", @@ -12248,6 +15624,22 @@ "dev": true, "license": "MIT" }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "dev": true, + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, "node_modules/concurrently": { "version": "10.0.3", "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.3.tgz", @@ -12559,6 +15951,67 @@ "node": ">=20" } }, + "node_modules/cross-fetch": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz", + "integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-fetch": "^2.7.0" + } + }, + "node_modules/cross-fetch/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/cross-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/cross-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true + }, + "node_modules/cross-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -12573,6 +16026,17 @@ "node": ">= 8" } }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": "*" + } + }, "node_modules/css-tree": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", @@ -12613,12 +16077,147 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, + "node_modules/csv-parse": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-7.0.1.tgz", + "integrity": "sha512-+2z7Ar0APQ7Uu6fX4cn+pitRmxjZ1WPBcGmZFKmA74FCyi7Et/XZx8cjNQ5CjbZ4HCOxXCOpRBYvYH08Qa003A==", + "dev": true, + "license": "MIT" + }, "node_modules/csv-stringify": { "version": "6.8.1", "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-6.8.1.tgz", "integrity": "sha512-tZ6X6TKQyQgCo5OptXcyAbfN1pwmoxEqELPQ7KFazNErx7kiVsDK8o+VYRXhfMl4N9vvOOLXuioquR2MeP847A==", "license": "MIT" }, + "node_modules/ctrf": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ctrf/-/ctrf-0.2.1.tgz", + "integrity": "sha512-iUo/eHcM5yG8aBS3Miqce9NNiZCtmVZxPpgmZEJIZ96bubwj7IpZx3IqsDqCH2FZjR71EH2NLtbBhtfzDjpaUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.20.0", + "ajv-formats": "3.0.1", + "glob": "13.0.6", + "yargs": "18.0.0" + }, + "bin": { + "ctrf": "dist/cli/cli.js" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/ctrf/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ctrf/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ctrf/node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/ctrf/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ctrf/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/ctrf/node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/ctrf/node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, "node_modules/cytoscape": { "version": "3.33.3", "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.3.tgz", @@ -13146,6 +16745,16 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/data-uri-to-buffer": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-8.0.0.tgz", + "integrity": "sha512-6UHfyCux51b8PTGDgveqtz1tvphBku5DrMKKJbFAZAJOI2zsjDpDoYE1+QGj7FOMS4BdTFNJsJiR3zEB0xH0yQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, "node_modules/data-urls": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", @@ -13229,6 +16838,26 @@ "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", "license": "MIT" }, + "node_modules/dayjs-plugin-utc": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dayjs-plugin-utc/-/dayjs-plugin-utc-0.1.2.tgz", + "integrity": "sha512-ExERH5o3oo6jFOdkvMP3gytTCQ9Ksi5PtylclJWghr7k7m3o2U5QrwtdiJkOxLOH4ghr0EKhpqGefzGz1VvVJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debounce": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-3.0.0.tgz", + "integrity": "sha512-64byRbF0/AirwbuHqB3/ZpMG9/nckDa6ZA0yd6UnaQNwbbemCOwvz2sL5sjXLHhZHADyiwLm0M5qMhltUUx+TA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -13276,8 +16905,8 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "devOptional": true, "license": "MIT", - "optional": true, "dependencies": { "mimic-response": "^3.1.0" }, @@ -13288,6 +16917,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -13332,6 +16976,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -13380,6 +17034,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/degenerator": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-7.0.1.tgz", + "integrity": "sha512-ABErK0IefDSyHjlPH7WUEenIAX2rPPnrDcDM+TS3z3+zu9TfyKKi07BQM+8rmxpdE2y1v5fjjdoAS/x4D2U60w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "quickjs-wasi": "^2.2.0" + } + }, "node_modules/delaunator": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", @@ -13530,6 +17202,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dpdm": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/dpdm/-/dpdm-4.2.0.tgz", @@ -13668,6 +17353,132 @@ "node": "^20.19.0 || ^22.12.0 || >=23" } }, + "node_modules/drizzle-orm": { + "version": "0.45.2", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz", + "integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=4", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1.13", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/sql.js": "*", + "@upstash/redis": ">=1.34.7", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "gel": ">=2", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "gel": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -13687,8 +17498,35 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true, - "license": "MIT", - "optional": true + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/editions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/editions/-/editions-6.22.0.tgz", + "integrity": "sha512-UgGlf8IW75je7HZjNDpJdCv4cGJWIi6yumFdZ0R7A8/CIhQiWUjyGLCxdHpd8bmyD1gnkfUNK0oeOXqUS2cpfQ==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "version-range": "^4.15.0" + }, + "engines": { + "ecmascript": ">= es5", + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } }, "node_modules/ee-first": { "version": "1.1.1", @@ -13725,6 +17563,13 @@ "node": ">= 4" } }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "dev": true, + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -13768,6 +17613,99 @@ "once": "^1.4.0" } }, + "node_modules/engine.io": { + "version": "6.6.9", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.9.tgz", + "integrity": "sha512-clKkw4C7nJ22mGgoVcCg6V/W/TxdNyIOTr89k2ONZu81qqkddPFDF0LXcbAwhzPD8DjkiRCjzuiO6Y+fkpD4vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "@types/ws": "^8.5.12", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.21.0" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.6.6", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.6.tgz", + "integrity": "sha512-iY6QdftLQ9pyiPoX082bpf/u1UewnOaJrtJIF9T0++QB34lZrj0uP+Q/bj8AlUsAxqhnkTV2BS8SBZSxOmoV5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.21.0", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/enhanced-resolve": { "version": "5.21.6", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", @@ -14031,6 +17969,17 @@ "license": "MIT", "optional": true }, + "node_modules/es6-promisify": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-7.0.0.tgz", + "integrity": "sha512-ginqzK3J90Rd4/Yz7qRrqUeIpe3TwSXTPPZtPne7tGBPeAaQiU8qt4fpKApnxHcq1AwtUdHVg5P77x/yrggG8Q==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/esast-util-from-estree": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", @@ -14131,6 +18080,13 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/escape-latex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz", + "integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==", + "dev": true, + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -14143,6 +18099,39 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/eslint": { "version": "9.39.4", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", @@ -14684,6 +18673,20 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", @@ -14842,12 +18845,32 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/eventemitter3": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "license": "MIT" }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/events-to-array": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/events-to-array/-/events-to-array-2.0.3.tgz", @@ -14871,9 +18894,9 @@ } }, "node_modules/eventsource-parser": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", - "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -15014,6 +19037,13 @@ "express": ">= 4.11" } }, + "node_modules/exsolve": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.0.tgz", + "integrity": "sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw==", + "dev": true, + "license": "MIT" + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -15105,6 +19135,13 @@ "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", "license": "MIT" }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "dev": true, + "license": "Unlicense" + }, "node_modules/fast-string-truncated-width": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", @@ -15122,6 +19159,20 @@ "fast-string-truncated-width": "^3.0.2" } }, + "node_modules/fast-unique-numbers": { + "version": "9.0.27", + "resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-9.0.27.tgz", + "integrity": "sha512-nDA9ADeINN8SA2u2wCtU+siWFTTDqQR37XvgPIDDmboWQeExz7X0mImxuaN+kJddliIqy2FpVRmnvRZ+j8i1/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.2.0" + } + }, "node_modules/fast-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", @@ -15148,6 +19199,57 @@ "fast-string-width": "^3.0.2" } }, + "node_modules/fast-xml-builder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.0.tgz", + "integrity": "sha512-F74cZEdCvuw9P41GAC3rod4X04jjWGM1JPEv/GWSqFTWLsdyMSBMBMlm9Hk3GLBgLBbdBNY8yee0pQh2RBVESQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.6.2", + "xml-naming": "^0.3.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.10.0.tgz", + "integrity": "sha512-SLhnTEqE5QpJHq/6zl9bsmImEP2adv+y6Wy+cJa7nVTRzQh1OZfCe9k29M5xN74LWnu0xa1zrUrq3KnOKl92Fg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.2.0", + "fast-xml-builder": "^1.2.0", + "is-unsafe": "^2.0.0", + "path-expression-matcher": "^1.6.2", + "strnum": "^2.4.1", + "xml-naming": "^0.3.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -15185,6 +19287,45 @@ } } }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/fetch-retry": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-5.0.6.tgz", + "integrity": "sha512-3yurQZ2hD9VISAhJJP9bpYFNQrHHBXE2JxxjY5aLEcDi46RmAzJE2OC9FAde0yis5ElW0jTTzs0zfg/Cca4XqQ==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/fetch-socks": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/fetch-socks/-/fetch-socks-1.3.3.tgz", @@ -15230,6 +19371,26 @@ "node": ">=16.0.0" } }, + "node_modules/file-type": { + "version": "21.3.2", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.2.tgz", + "integrity": "sha512-DLkUvGwep3poOV2wpzbHCOnSKGk1LzyXTv+aHFgN2VFl96wnp8YA9YjO2qPzg5PuL8q/SW9Pdi6WTkYOIh995w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -15237,6 +19398,16 @@ "license": "MIT", "optional": true }, + "node_modules/filesize": { + "version": "10.1.6", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-10.1.6.tgz", + "integrity": "sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 10.4.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -15321,6 +19492,13 @@ "dev": true, "license": "ISC" }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "dev": true, + "license": "MIT" + }, "node_modules/follow-redirects": { "version": "1.16.0", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", @@ -15427,6 +19605,19 @@ "node": ">=18.3.0" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -15436,6 +19627,20 @@ "node": ">= 0.6" } }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/framer-motion": { "version": "12.42.2", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.42.2.tgz", @@ -15795,6 +20000,90 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gaxios/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/gaxios/node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/gaxios/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/gaxios/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.3.tgz", + "integrity": "sha512-ziTrzUhhpL9Zk5k0HHzgP/KIpWDJT0VMBC/ynt/QIBvTW+UUcSivQRl6VlwTf/EilDxtSWklHoRsKy1c4k+59w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "gaxios": "7.1.3", + "google-logging-utils": "1.1.3", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/generator-function": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", @@ -15929,6 +20218,21 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/get-uri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-8.0.1.tgz", + "integrity": "sha512-/5N/P4Lrh0p/mDwlDRi7Y1+P2o/OyzZI3l6Iz1Ov6XXwwm1y3RlZLuo3gVgML99djrEDtV980bBxSuOeHLk8ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.3.1", + "data-uri-to-buffer": "8.0.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -16097,6 +20401,257 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/globby": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", + "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.3", + "path-type": "^6.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/globby/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/globby/node_modules/path-type": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", + "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/google-auth-library": { + "version": "10.9.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.9.0.tgz", + "integrity": "sha512-xtvUqvINPhTaBm7nXqlYPcrMHJPm1lCNdSovxnKKhTm+4JsvQ+KGVYJViLoH9Yxu8w+T0Qv5HubzYT9BLrppJg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-auth-library/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/google-auth-library/node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/google-auth-library/node_modules/gaxios": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.2.0.tgz", + "integrity": "sha512-CUVb4wcYe+771XevyH6HtGmXFAGGKkIC3kswAP8Z1JCe0j80JMaTPZH930DWFrvo0atjh18Arc0pEyUCWa5bfg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-auth-library/node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-auth-library/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/google-auth-library/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/google-protobuf": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.4.tgz", + "integrity": "sha512-MnG7N936zcKTco4Jd2PX2U96Kf9PxygAPKBug+74LHzmHXmceN16MmRcdgZv+DGef/S9YvQAfRsNCn4cjf9yyQ==", + "dev": true, + "license": "(BSD-3-Clause AND Apache-2.0)" + }, + "node_modules/googleapis-common": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-8.0.2.tgz", + "integrity": "sha512-5MXeQzIZaqCH7B+HJWqhQm946VARpZep6acbWSr/fcgF2cQANq7allgX+i/G0EqF0WyUxB277gtWMzRYHMl9tg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "extend": "^3.0.2", + "gaxios": "7.1.3", + "google-auth-library": "10.5.0", + "google-logging-utils": "1.1.3", + "qs": "^6.7.0", + "url-template": "^2.0.8" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/googleapis-common/node_modules/google-auth-library": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", + "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^8.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -16109,6 +20664,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, "node_modules/gpt-tokenizer": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/gpt-tokenizer/-/gpt-tokenizer-3.4.0.tgz", @@ -16121,6 +20702,43 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/grpc-js-reflection-client": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/grpc-js-reflection-client/-/grpc-js-reflection-client-1.4.0.tgz", + "integrity": "sha512-zkypuxNu0u53rhiS9PpP0ih8gZTmy/+yxHU6U+isWBX311YcE2nzn9xA4prkGsNYf5d8Q5MF2duVbvS/HMm9cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "google-protobuf": "^4.0.2", + "lodash": "^4.18.1", + "protobufjs": "^8.0.1" + }, + "peerDependencies": { + "@grpc/grpc-js": "^1.14.3" + } + }, + "node_modules/grpc-js-reflection-client/node_modules/google-protobuf": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-4.0.2.tgz", + "integrity": "sha512-yD2fqbNgvJPuQwdKJiPdbUcXveNRxgqy070gzsBsCyFJA8Qdj9oxa9xtkddb/JEhcDk0RD5SfGUWg+nhINfMxA==", + "dev": true, + "license": "(BSD-3-Clause AND Apache-2.0)" + }, + "node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/guid-typescript": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz", @@ -16212,6 +20830,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/hasown": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", @@ -16489,6 +21120,23 @@ "node": ">=16.9.0" } }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/hookpoint": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hookpoint/-/hookpoint-4.1.0.tgz", + "integrity": "sha512-nKlElb27LmBWAVWirkcX8cQNGAPLsBe0pk4uTBt9faaOTFyUsMCWtKq6gAx8GlapPTGQ0rOnbIJQFSWqX1xIuw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, "node_modules/hosted-git-info": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", @@ -16619,6 +21267,38 @@ "node": "^22.15.0 || ^24.0.0 || >=26.0.0" } }, + "node_modules/http-status-codes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", + "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/http-z": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/http-z/-/http-z-8.1.1.tgz", + "integrity": "sha512-4rEIu4SljSAs+lgCzzskyNdYllteGIHdnMBsu9MqafivyPAofSmCsrRjHQgxLs0BoPkUJBa7Ld6rXP32SPI8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, "node_modules/https-proxy-agent": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.1.0.tgz", @@ -16639,6 +21319,187 @@ "integrity": "sha512-URfeibL0kTH6VuIxxaJDXWQWEk8fKr+9L8MGv6CuAiNy0fGnoVhWbXBvJR1mkdsvCDUxvhX9cW60k2AhtH5s6w==", "license": "MIT" }, + "node_modules/httpyac": { + "version": "6.16.7", + "resolved": "https://registry.npmjs.org/httpyac/-/httpyac-6.16.7.tgz", + "integrity": "sha512-aJooJeQioieWTB8LAqUNFIUo5WM8yJzLEoPtBZFhvvCZEXo5r5/sswMFfX1Et7P6IrISRQywU3a4s8unjfAqpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cloudamqp/amqp-client": "^2.1.1", + "@grpc/grpc-js": "^1.12.2", + "@grpc/proto-loader": "^0.7.13", + "@xmldom/xmldom": "^0.9.5", + "aws4": "^1.13.2", + "chalk": "^4.1.2", + "clipboardy": "^4.0.0", + "commander": "^12.1.0", + "dayjs": "^1.11.13", + "dayjs-plugin-utc": "^0.1.2", + "dotenv": "^16.4.5", + "encodeurl": "^2.0.0", + "eventsource": "^2.0.2", + "filesize": "^10.1.6", + "globby": "^14.0.2", + "google-protobuf": "^3.21.4", + "got": "^11.8.6", + "grpc-js-reflection-client": "^1.2.20", + "hookpoint": "4.1.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "inquirer": "^12.0.1", + "kafkajs": "^2.2.4", + "lodash": "^4.17.21", + "mqtt": "^5.10.1", + "open": "^8.4.2", + "socks-proxy-agent": "^8.0.4", + "tough-cookie": "^5.0.0", + "uuid": "^11.0.1", + "ws": "^8.18.0", + "xmldom-format": "^2.0.0", + "xpath": "^0.0.34" + }, + "bin": { + "httpyac": "bin/httpyac.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/httpyac/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/httpyac/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/httpyac/node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/httpyac/node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/httpyac/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/httpyac/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/httpyac/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/httpyac/node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/httpyac/node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/httpyac/node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/httpyac/node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/human-signals": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", @@ -16665,6 +21526,216 @@ "url": "https://github.com/sponsors/typicode" } }, + "node_modules/ibm-cloud-sdk-core": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/ibm-cloud-sdk-core/-/ibm-cloud-sdk-core-5.5.0.tgz", + "integrity": "sha512-ot6sGHAvSnd/ZSU4ZFn+m7i+xcFo3pmA3qm2JmEndDkMsuNR8HLdUeJ5iBbmkUfCGbf2ccTu/GvoJgFetyO6XA==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@types/debug": "4.1.12", + "@types/node": "18.19.80", + "@types/tough-cookie": "4.0.0", + "axios": "1.18.0", + "camelcase": "6.3.0", + "debug": "4.3.4", + "dotenv": "16.4.5", + "extend": "3.0.2", + "file-type": "21.3.2", + "form-data": "4.0.6", + "isstream": "0.1.2", + "jsonwebtoken": "9.0.3", + "load-esm": "1.0.3", + "mime-types": "2.1.35", + "retry-axios": "2.6.0", + "tough-cookie": "4.1.3" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/ibm-cloud-sdk-core/node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/ibm-cloud-sdk-core/node_modules/@types/node": { + "version": "18.19.80", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.80.tgz", + "integrity": "sha512-kEWeMwMeIvxYkeg1gTc01awpwLbfMRZXdIhwRcakd/KlK53jmRC26LqcbIt7fnAQTu5GzlnWmzA3H6+l1u6xxQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/ibm-cloud-sdk-core/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ibm-cloud-sdk-core/node_modules/axios": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.0.tgz", + "integrity": "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/ibm-cloud-sdk-core/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ibm-cloud-sdk-core/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/ibm-cloud-sdk-core/node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/ibm-cloud-sdk-core/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ibm-cloud-sdk-core/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ibm-cloud-sdk-core/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ibm-cloud-sdk-core/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/ibm-cloud-sdk-core/node_modules/tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ibm-cloud-sdk-core/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/ibm-cloud-sdk-core/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", @@ -16700,6 +21771,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "devOptional": true, "funding": [ { "type": "github", @@ -16714,8 +21786,7 @@ "url": "https://feross.org/support" } ], - "license": "BSD-3-Clause", - "optional": true + "license": "BSD-3-Clause" }, "node_modules/ignore": { "version": "5.3.2", @@ -17126,6 +22197,453 @@ "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", "license": "MIT" }, + "node_modules/inquirer": { + "version": "12.11.1", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-12.11.1.tgz", + "integrity": "sha512-9VF7mrY+3OmsAfjH3yKz/pLbJ5z22E23hENKw3/LNSaA/sAt3v49bDRY+Ygct1xwuKT+U+cBfTzjCPySna69Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/prompts": "^7.10.1", + "@inquirer/type": "^3.0.10", + "mute-stream": "^2.0.0", + "run-async": "^4.0.6", + "rxjs": "^7.8.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/inquirer/node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/inquirer/node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/inquirer/node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/inquirer/node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/inquirer/node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/inquirer/node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/inquirer/node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/inquirer/node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/inquirer/node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/inquirer/node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/inquirer/node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/inquirer/node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/inquirer/node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/inquirer/node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/inquirer/node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/inquirer/node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/inquirer/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/inquirer/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/inquirer/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -17307,6 +22825,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/is-bun-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", @@ -17418,6 +22944,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-electron": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-electron/-/is-electron-2.2.2.tgz", + "integrity": "sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/is-expression": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz", @@ -17828,6 +23362,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-unsafe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.0.tgz", + "integrity": "sha512-2LdV822R+wmI86unXA93WCFpL6g+av8ynWk0nrHyJqGop5VoocYsSLFgN8jrfalT6iGeLNM4KXuVSsULP53kEA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -17889,6 +23436,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is64bit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is64bit/-/is64bit-2.0.0.tgz", + "integrity": "sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "system-architecture": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -17902,6 +23465,14 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -17954,6 +23525,24 @@ "node": ">=8" } }, + "node_modules/istextorbinary": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-9.5.0.tgz", + "integrity": "sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "binaryextensions": "^6.11.0", + "editions": "^6.21.0", + "textextensions": "^6.11.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, "node_modules/iterator.prototype": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", @@ -17978,7 +23567,6 @@ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, "license": "BlueOak-1.0.0", - "optional": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -17989,6 +23577,13 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==", + "dev": true, + "license": "MIT" + }, "node_modules/jiti": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", @@ -17999,6 +23594,19 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jks-js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/jks-js/-/jks-js-1.1.7.tgz", + "integrity": "sha512-BeiDRKsAi1NwEwgx2JB/9/0tar5BNGIv+foGm1G5GgiyR35s/iUnfd/BWqYd16mLDD8qTaAVBrIcOOuVqXJZNQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-forge": "^1.4.0", + "node-int64": "^0.4.0", + "node-rsa": "^1.1.1" + } + }, "node_modules/joi": { "version": "18.2.1", "resolved": "https://registry.npmjs.org/joi/-/joi-18.2.1.tgz", @@ -18036,6 +23644,13 @@ "node": ">=10" } }, + "node_modules/js-base64": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.8.1.tgz", + "integrity": "sha512-5xVjhUZlHHeuO2W7w2rDFj/Kl1xLX+HjZxdOQwCsUOifl6UaoH1o1wsbsTMz+r0aeC7gCijvru02j6TfKZWzKg==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/js-md4": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz", @@ -18043,6 +23658,27 @@ "dev": true, "license": "MIT" }, + "node_modules/js-rouge": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/js-rouge/-/js-rouge-3.2.0.tgz", + "integrity": "sha512-2dvY28iFq5NcwxPNzc2zMgLVJED843m6CnKrCy0jYnOKd+QQhdkxI1wmdQspbcOAggo3K3gUZfhTSwmM+lWoBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/js-sdsl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", + "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/js-stringify": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", @@ -18194,6 +23830,16 @@ "node": ">=6" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -18214,6 +23860,27 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -18292,6 +23959,44 @@ ], "license": "MIT" }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jstransformer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", @@ -18336,6 +24041,243 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/junit-to-ctrf": { + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/junit-to-ctrf/-/junit-to-ctrf-0.0.14.tgz", + "integrity": "sha512-gVrJaMKhE2tKuHh9Of/Le1Y6s6U8aZy9HUapW+IjbltyvomsGBoabv0Ul7xfTlZshYCYGbwx6OWFNm2Q66EaHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ctrf": "^0.0.17", + "fs-extra": "^11.3.0", + "glob": "^11.0.3", + "typescript": "^5.8.3", + "xml2js": "^0.6.2", + "yargs": "^18.0.0" + }, + "bin": { + "junit-to-ctrf": "dist/cli.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/junit-to-ctrf/node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/junit-to-ctrf/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/junit-to-ctrf/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/junit-to-ctrf/node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/junit-to-ctrf/node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/junit-to-ctrf/node_modules/ctrf": { + "version": "0.0.17", + "resolved": "https://registry.npmjs.org/ctrf/-/ctrf-0.0.17.tgz", + "integrity": "sha512-PPk9b+AuA+UoBcbzSQWXMIuh5601zDHgXlmHIG8ESxTUnnb0eM2sz8H3jQLYQZTpyIaZVCLFZFslIDB1EMVZ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "glob": "^11.0.3", + "typescript": "^5.8.3", + "yargs": "^18.0.0" + }, + "bin": { + "ctrf": "dist/cli.js" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/junit-to-ctrf/node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/junit-to-ctrf/node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/junit-to-ctrf/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/junit-to-ctrf/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/junit-to-ctrf/node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/junit-to-ctrf/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/junit-to-ctrf/node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/junit-to-ctrf/node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, "node_modules/just-diff": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/just-diff/-/just-diff-6.0.2.tgz", @@ -18350,6 +24292,52 @@ "dev": true, "license": "MIT" }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kafkajs": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/kafkajs/-/kafkajs-2.2.4.tgz", + "integrity": "sha512-j/YeapB1vfPT2iOIUn/vxdyKEuhuY2PxMBvf5JWux6iSaukAccrMtXEY/Lb7OvavDhOWME589bpLrEdnVHjfjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/kareem": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-3.3.0.tgz", + "integrity": "sha512-kpSuLD3/7RenBnjnJdOHXCKC8dTd1JzeOiJhN0necWWci6cC+qX+VuwPnMVgb+a4+KNJSfgqahpnfWaeDXCimw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/katex": { "version": "0.16.45", "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.45.tgz", @@ -18397,6 +24385,24 @@ "json-buffer": "3.0.1" } }, + "node_modules/keyv-file": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/keyv-file/-/keyv-file-5.3.5.tgz", + "integrity": "sha512-0JFTTi55d1HdhIrSOnPngUw0fyHLn3BHqoLJ8TyGKM/fQfuZsz8HkcFxpl6YzU2mj1ZfRF/6BXlSqOpyfXYAmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1", + "tslib": "^1.14.1" + } + }, + "node_modules/keyv-file/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, "node_modules/khroma": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", @@ -18465,6 +24471,13 @@ "url": "https://liberapay.com/Koromix" } }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "dev": true, + "license": "MIT" + }, "node_modules/ky": { "version": "1.14.3", "resolved": "https://registry.npmjs.org/ky/-/ky-1.14.3.tgz", @@ -18477,6 +24490,34 @@ "url": "https://github.com/sindresorhus/ky?sponsor=1" } }, + "node_modules/langfuse": { + "version": "3.38.20", + "resolved": "https://registry.npmjs.org/langfuse/-/langfuse-3.38.20.tgz", + "integrity": "sha512-MAmBAASSzJtmK1O9HQegA1mFsQhT8Yf+OJRGvE7FXkyv3g/eiBE0glLD0Ohg3pkxhoPdggM5SejK7ue9ctlaMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "langfuse-core": "^3.38.20" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/langfuse-core": { + "version": "3.38.20", + "resolved": "https://registry.npmjs.org/langfuse-core/-/langfuse-core-3.38.20.tgz", + "integrity": "sha512-zBKVmQN/1oT5VWZUBYlWzvokIlkC/6mnpgr/2atMyTeAm+jR3ia7w2iJMjlrF5/oG8ukO1s8+LDRCzJpF1QeEA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "mustache": "^4.2.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -18532,6 +24573,49 @@ "node": ">= 0.8.0" } }, + "node_modules/libsql": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/libsql/-/libsql-0.5.29.tgz", + "integrity": "sha512-8lMP8iMgiBzzoNbAPQ59qdVcj6UaE/Vnm+fiwX4doX4Narook0a4GPKWBEv+CR8a1OwbfkgL18uBfBjWdF0Fzg==", + "cpu": [ + "x64", + "arm64", + "wasm32", + "arm" + ], + "dev": true, + "license": "MIT", + "os": [ + "darwin", + "linux", + "win32" + ], + "dependencies": { + "@neon-rs/load": "^0.0.4", + "detect-libc": "2.0.2" + }, + "optionalDependencies": { + "@libsql/darwin-arm64": "0.5.29", + "@libsql/darwin-x64": "0.5.29", + "@libsql/linux-arm-gnueabihf": "0.5.29", + "@libsql/linux-arm-musleabihf": "0.5.29", + "@libsql/linux-arm64-gnu": "0.5.29", + "@libsql/linux-arm64-musl": "0.5.29", + "@libsql/linux-x64-gnu": "0.5.29", + "@libsql/linux-x64-musl": "0.5.29", + "@libsql/win32-x64-msvc": "0.5.29" + } + }, + "node_modules/libsql/node_modules/detect-libc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/libxmljs2": { "version": "0.37.0", "resolved": "https://registry.npmjs.org/libxmljs2/-/libxmljs2-0.37.0.tgz", @@ -19273,6 +25357,27 @@ "node": ">=22.13.0" } }, + "node_modules/load-esm": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/load-esm/-/load-esm-1.0.3.tgz", + "integrity": "sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + }, + { + "type": "buymeacoffee", + "url": "https://buymeacoffee.com/borewit" + } + ], + "license": "MIT", + "optional": true, + "engines": { + "node": ">=13.2.0" + } + }, "node_modules/loader-utils": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", @@ -19432,6 +25537,13 @@ "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", "license": "MIT" }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -19446,6 +25558,54 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -19453,6 +25613,14 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/log-symbols": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", @@ -19555,6 +25723,34 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/logform/node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/long": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", @@ -19600,6 +25796,16 @@ "url": "https://github.com/sponsors/typicode" } }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -19789,6 +25995,43 @@ "node": ">= 0.4" } }, + "node_modules/mathjs": { + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-15.2.0.tgz", + "integrity": "sha512-UAQzSVob9rNLdGpqcFMYmSu9dkuLYy7Lr2hBEQS5SHQdknA9VppJz3cy2KkpMzTODunad6V6cNv+5kOLsePLow==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.26.10", + "complex.js": "^2.2.5", + "decimal.js": "^10.4.3", + "escape-latex": "^1.2.0", + "fraction.js": "^5.2.1", + "javascript-natural-sort": "^0.7.1", + "seedrandom": "^3.0.5", + "tiny-emitter": "^2.1.0", + "typed-function": "^4.2.1" + }, + "bin": { + "mathjs": "bin/cli.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "node_modules/mdast-util-find-and-replace": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", @@ -20104,6 +26347,25 @@ "node": ">= 0.8" } }, + "node_modules/memjs": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/memjs/-/memjs-1.3.2.tgz", + "integrity": "sha512-qUEg2g8vxPe+zPn09KidjIStHPtoBO8Cttm8bgJFWWabbsjQ9Av9Ky+6UcvKx6ue0LLb/LEhtcyQpRyKfzeXcg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/merge-descriptors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", @@ -20966,8 +27228,8 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "devOptional": true, "license": "MIT", - "optional": true, "engines": { "node": ">=10" }, @@ -21201,6 +27463,133 @@ "node": ">= 18" } }, + "node_modules/mongodb": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.2.0.tgz", + "integrity": "sha512-F/2+BMZtLVhY30ioZp0dAmZ+IRZMBqI+nrv6t5+9/1AIwCa8sMRC3jBf81lpxMhnZgqq8CoUD503Z1oZWq1/sw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^7.2.0", + "mongodb-connection-string-url": "^7.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.806.0", + "@mongodb-js/zstd": "^7.0.0", + "gcp-metadata": "^7.0.1", + "kerberos": "^7.0.0", + "mongodb-client-encryption": ">=7.0.0 <7.1.0", + "snappy": "^7.3.2", + "socks": "^2.8.6" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.1.tgz", + "integrity": "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@types/whatwg-url": "^13.0.0", + "whatwg-url": "^14.1.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/mongodb-connection-string-url/node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/mongodb-connection-string-url/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/mongodb-connection-string-url/node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/mongoose": { + "version": "9.7.4", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.7.4.tgz", + "integrity": "sha512-nuSYGUWWzNd4EAbGYxE469wPTL+kmxb5+91YvCvMkJ08rvNRht/usZUU3LuFuk7rDutF2QWBZHPHuzM8TxXApA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "kareem": "3.3.0", + "mongodb": "~7.2", + "mpath": "0.9.0", + "mquery": "6.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, "node_modules/moo": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.3.tgz", @@ -21250,12 +27639,194 @@ "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==", "license": "MIT" }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mqtt": { + "version": "5.15.2", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.15.2.tgz", + "integrity": "sha512-VWZU2CSUY3U3oN0PSBRDE5SNsFi4zqqNeQ/uv3pZWqY3CrBXD/dhd0ZLjlsk5YnebGlrapi4lRVNJPUNQ5aZ3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/readable-stream": "^4.0.21", + "@types/ws": "^8.18.1", + "commist": "^3.2.0", + "concat-stream": "^2.0.0", + "debug": "^4.4.1", + "help-me": "^5.0.0", + "lru-cache": "^10.4.3", + "minimist": "^1.2.8", + "mqtt-packet": "^9.0.2", + "number-allocator": "^1.0.14", + "readable-stream": "^4.7.0", + "rfdc": "^1.4.1", + "socks": "^2.8.6", + "split2": "^4.2.0", + "worker-timers": "^8.0.23", + "ws": "^8.18.3" + }, + "bin": { + "mqtt": "build/bin/mqtt.js", + "mqtt_pub": "build/bin/pub.js", + "mqtt_sub": "build/bin/sub.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/mqtt-packet": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-9.0.2.tgz", + "integrity": "sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^6.0.8", + "debug": "^4.3.4", + "process-nextick-args": "^2.0.1" + } + }, + "node_modules/mqtt-packet/node_modules/bl": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz", + "integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/readable-stream": "^4.0.0", + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^4.2.0" + } + }, + "node_modules/mqtt-packet/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/mqtt-packet/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/mqtt/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/mqtt/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/mqtt/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/mquery": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-6.0.0.tgz", + "integrity": "sha512-b2KQNsmgtkscfeDgkYMcWGn9vZI9YoXh802VDEwE6qc50zxBFQ0Oo8ROkawbPAsXCY1/Z1yp0MagqsZStPWJjw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "mustache": "bin/mustache" + } + }, "node_modules/mutation-server-protocol": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/mutation-server-protocol/-/mutation-server-protocol-0.4.1.tgz", @@ -21373,6 +27944,33 @@ "url": "https://opencollective.com/napi-postinstall" } }, + "node_modules/natural": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/natural/-/natural-8.1.1.tgz", + "integrity": "sha512-Ucb+lsUcGxUqu3rn8cwHjT6gJQosO63nIX/aBQXB3+IDkNbFV7PuviysO+Rzz3aKn7PZhPj3bNF4PS9gDVjYCQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "afinn-165": "^2.0.2", + "afinn-165-financialmarketnews": "^3.0.0", + "apparatus": "^0.0.10", + "dotenv": "^17.3.1", + "memjs": "^1.3.2", + "mongoose": "^9.2.1", + "pg": "^8.18.0", + "redis": "^5.11.0", + "safe-stable-stringify": "^2.5.0", + "stopwords-iso": "^1.1.0", + "sylvester": "^0.0.21", + "underscore": "^1.13.0", + "uuid": "^13.0.0", + "wordnet-db": "^3.1.14" + }, + "engines": { + "node": ">=0.4.10" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -21380,6 +27978,20 @@ "dev": true, "license": "MIT" }, + "node_modules/natural/node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/nearley": { "version": "2.20.1", "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", @@ -21421,6 +28033,16 @@ "node": ">= 0.6" } }, + "node_modules/netmask": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", + "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/next": { "version": "16.2.10", "resolved": "https://registry.npmjs.org/next/-/next-16.2.10.tgz", @@ -21563,6 +28185,27 @@ "license": "MIT", "optional": true }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-emoji": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", @@ -21643,6 +28286,17 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/node-forge": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", + "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", + "dev": true, + "license": "(BSD-3-Clause OR GPL-2.0)", + "optional": true, + "engines": { + "node": ">= 6.13.0" + } + }, "node_modules/node-gyp": { "version": "12.4.0", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz", @@ -21743,6 +28397,14 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/node-loader": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/node-loader/-/node-loader-2.1.0.tgz", @@ -21779,6 +28441,17 @@ "node": ">=18" } }, + "node_modules/node-rsa": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/node-rsa/-/node-rsa-1.1.1.tgz", + "integrity": "sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "asn1": "^0.2.4" + } + }, "node_modules/node-sarif-builder": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/node-sarif-builder/-/node-sarif-builder-4.1.0.tgz", @@ -21793,6 +28466,21 @@ "node": ">=20" } }, + "node_modules/node-sql-parser": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/node-sql-parser/-/node-sql-parser-5.4.0.tgz", + "integrity": "sha512-jVe6Z61gPcPjCElPZ6j8llB3wnqGcuQzefim1ERsqIakxnEy5JlzV7XKdO1KmacRG5TKwPc4vJTgSRQ0LfkbFw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@types/pegjs": "^0.10.0", + "big-integer": "^1.6.48" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/nopt": { "version": "7.2.1", "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", @@ -21847,6 +28535,19 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/npm-bundled": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-5.0.0.tgz", @@ -22001,6 +28702,53 @@ "node": ">=8" } }, + "node_modules/number-allocator": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz", + "integrity": "sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.1", + "js-sdsl": "4.3.0" + } + }, + "node_modules/nunjucks": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.4.tgz", + "integrity": "sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "a-sync-waterfall": "^1.0.0", + "asap": "^2.0.3", + "commander": "^5.1.0" + }, + "bin": { + "nunjucks-precompile": "bin/precompile" + }, + "engines": { + "node": ">= 6.9.0" + }, + "peerDependencies": { + "chokidar": "^3.3.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/nunjucks/node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -22179,6 +28927,16 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -22188,6 +28946,16 @@ "wrappy": "1" } }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, "node_modules/onetime": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", @@ -22294,6 +29062,66 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openai": { + "version": "6.46.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.46.0.tgz", + "integrity": "sha512-DFg6jEPT2RO+oAyXtddeUJU8zkGy1OQ1AjGzNIJUMQG03TTqvCpy9tBpQ+2VVVnvrl3E56F8GEin2JYtWpITtA==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/credential-provider-node": ">=3.972.0 <4", + "@smithy/hash-node": ">=4.3.0 <5", + "@smithy/signature-v4": ">=5.4.0 <6", + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-provider-node": { + "optional": true + }, + "@smithy/hash-node": { + "optional": true + }, + "@smithy/signature-v4": { + "optional": true + }, + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/openapi-fetch": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/openapi-fetch/-/openapi-fetch-0.8.2.tgz", + "integrity": "sha512-4g+NLK8FmQ51RW6zLcCBOVy/lwYmFJiiT+ckYZxJWxUxH4XFhsNcX2eeqVMfVOi+mDNFja6qDXIZAz2c5J/RVw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "openapi-typescript-helpers": "^0.0.5" + } + }, + "node_modules/openapi-typescript-helpers": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/openapi-typescript-helpers/-/openapi-typescript-helpers-0.0.5.tgz", + "integrity": "sha512-MRffg93t0hgGZbYTxg60hkRIK2sRuEOHEtCUgMuLgbCC33TMQ68AmxskzUlauzZYD47+ENeGV/ElI7qnWqrAxA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "license": "(WTFPL OR MIT)", + "bin": { + "opener": "bin/opener-bin.js" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -22346,6 +29174,17 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -22443,6 +29282,27 @@ "@oxc-resolver/binding-win32-x64-msvc": "11.21.3" } }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -22488,6 +29348,165 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue-compat": { + "version": "1.0.225", + "resolved": "https://registry.npmjs.org/p-queue-compat/-/p-queue-compat-1.0.225.tgz", + "integrity": "sha512-SdfGSQSJJpD7ZR+dJEjjn9GuuBizHPLW/yarJpXnmrHRruzrq7YM8OqsikSrKeoPv+Pi1YXw9IIBSIg5WveQHA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "eventemitter3": "5.x", + "p-timeout-compat": "^1.0.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/p-queue/node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-retry/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-timeout-compat": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/p-timeout-compat/-/p-timeout-compat-1.0.8.tgz", + "integrity": "sha512-+7LpKr1ilnWU0LbV2r+Wz4srwMcFTUysmgL824ZxJcZP3u4Hyi/D/39pbyEs4j0XXCHvbv069+LDPxlCijfVRQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/pac-proxy-agent": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-9.1.0.tgz", + "integrity": "sha512-1aU+1mpj3DrQPfo3gh+3Gap3G5x+axnMx1P/y0ZF2ch7kb2meyOCAH8K2k9d27ROsTE7TnAerzxqF9aon2jqnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4", + "get-uri": "8.0.1", + "http-proxy-agent": "9.1.0", + "https-proxy-agent": "9.1.0", + "pac-resolver": "9.0.1", + "quickjs-wasi": "^2.2.0", + "socks-proxy-agent": "10.1.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/pac-proxy-agent/node_modules/http-proxy-agent": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-9.1.0.tgz", + "integrity": "sha512-2NxoveTT58mjYT4n3RPTEfCZGLMbidoO8XEieXfpSYxu+PQJ1qpx4ypwH6N+uF9twBPIvRRgvkvW5HUTYWENig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4", + "proxy-agent-negotiate": "1.1.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/pac-proxy-agent/node_modules/socks-proxy-agent": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-10.1.0.tgz", + "integrity": "sha512-WlMj/67cEJ6MDI1OcsnjuYKDNDoyPCCYZ249kuuXPiMDw9F8PXkVaQ7YWu3siTydfQ/4BEZcvGzu+aYvz7dDCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/pac-resolver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-9.0.1.tgz", + "integrity": "sha512-lJbS008tmkj08VhoM8Hzuv/VE5tK9MS0OIQ/7+s0lIF+BYhiQWFYzkSpML7lXs9iBu2jfmzBTLzhe9n6BX+dYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "degenerator": "7.0.1", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "quickjs-wasi": "^2.2.0" + } + }, "node_modules/package-json": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/package-json/-/package-json-10.0.1.tgz", @@ -22511,8 +29530,7 @@ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true, - "license": "BlueOak-1.0.0", - "optional": true + "license": "BlueOak-1.0.0" }, "node_modules/package-json/node_modules/semver": { "version": "7.8.0", @@ -22725,6 +29743,22 @@ "node": ">=8" } }, + "node_modules/path-expression-matcher": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz", + "integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -22792,6 +29826,163 @@ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "license": "MIT" }, + "node_modules/pdf-parse": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-2.4.5.tgz", + "integrity": "sha512-mHU89HGh7v+4u2ubfnevJ03lmPgQ5WU4CxAVmTSh/sxVTEDYd1er/dKS/A6vg77NX47KTEoihq8jZBLr8Cxuwg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@napi-rs/canvas": "0.1.80", + "pdfjs-dist": "5.4.296" + }, + "bin": { + "pdf-parse": "bin/cli.mjs" + }, + "engines": { + "node": ">=20.16.0 <21 || >=22.3.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/mehmet-kozan" + } + }, + "node_modules/pdfjs-dist": { + "version": "5.4.296", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz", + "integrity": "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=20.16.0 || >=22.3.0" + }, + "optionalDependencies": { + "@napi-rs/canvas": "^0.1.80" + } + }, + "node_modules/pem": { + "version": "1.14.8", + "resolved": "https://registry.npmjs.org/pem/-/pem-1.14.8.tgz", + "integrity": "sha512-ZpbOf4dj9/fQg5tQzTqv4jSKJQsK7tPl0pm4/pvPcZVjZcJg7TMfr3PBk6gJH97lnpJDu4e4v8UUqEz5daipCg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es6-promisify": "^7.0.0", + "md5": "^2.3.0", + "os-tmpdir": "^1.0.2", + "which": "^2.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/pg": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz", + "integrity": "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "pg-connection-string": "^2.14.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.15.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz", + "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "dev": true, + "license": "ISC", + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "dev": true, + "license": "MIT", + "optional": true, + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.15.0.tgz", + "integrity": "sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -22969,6 +30160,42 @@ "node": ">=18" } }, + "node_modules/playwright-ctrf-json-reporter": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/playwright-ctrf-json-reporter/-/playwright-ctrf-json-reporter-0.0.29.tgz", + "integrity": "sha512-zwJx7y/StmMtVq4DHbn85II/YbhnufyL08CeBH0sPzwq2nmHxsaRj+Te+ISimnWsHptrvcWRIfueh1LMjVwuIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ctrf": "^0.2.0" + } + }, + "node_modules/playwright-extra": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/playwright-extra/-/playwright-extra-4.3.6.tgz", + "integrity": "sha512-q2rVtcE8V8K3vPVF1zny4pvwZveHLH8KBuVU2MoE3Jw4OKVoBWsHI9CH9zPydovHHOCDxjGN2Vg+2m644q3ijA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "playwright": "*", + "playwright-core": "*" + }, + "peerDependenciesMeta": { + "playwright": { + "optional": true + }, + "playwright-core": { + "optional": true + } + } + }, "node_modules/po-parser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/po-parser/-/po-parser-2.1.1.tgz", @@ -23055,6 +30282,66 @@ "node": ">=4" } }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/posthog-node": { + "version": "5.24.17", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.24.17.tgz", + "integrity": "sha512-mdb8TKt+YCRbGQdYar3AKNUPCyEiqcprScF4unYpGALF6HlBaEuO6wPuIqXXpCWkw4VclJYCKbb6lq6pH6bJeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@posthog/core": "1.23.1" + }, + "engines": { + "node": "^20.20.0 || >=22.22.0" + } + }, "node_modules/powershell-utils": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", @@ -23147,6 +30434,23 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, "node_modules/process-warning": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", @@ -23213,6 +30517,13 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/promise-limit": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/promise-limit/-/promise-limit-2.7.0.tgz", + "integrity": "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==", + "dev": true, + "license": "ISC" + }, "node_modules/promise-retry": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", @@ -23228,6 +30539,1091 @@ "node": ">=10" } }, + "node_modules/promptfoo": { + "version": "0.121.18", + "resolved": "https://registry.npmjs.org/promptfoo/-/promptfoo-0.121.18.tgz", + "integrity": "sha512-avytaJ3Vi043Cp/LHRNstKK7PzaDso5QvPa1llMAsISfG8uC7w3mKATGlLcO8Qo6SIhmibfz2JUQvG0EuFuJ0g==", + "dev": true, + "license": "MIT", + "workspaces": [ + "src/app", + "site" + ], + "dependencies": { + "@anthropic-ai/sdk": "0.106.0", + "@apidevtools/json-schema-ref-parser": "^15.3.1", + "@inquirer/checkbox": "^5.1.0", + "@inquirer/confirm": "^6.0.8", + "@inquirer/core": "^11.1.5", + "@inquirer/editor": "^5.0.8", + "@inquirer/input": "^5.0.8", + "@inquirer/search": "^4.1.8", + "@inquirer/select": "^5.1.0", + "@libsql/client": "^0.17.3", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.219.0", + "@opentelemetry/resources": "^2.6.0", + "@opentelemetry/sdk-trace-base": "^2.6.0", + "@opentelemetry/sdk-trace-node": "^2.6.0", + "@opentelemetry/semantic-conventions": "^1.40.0", + "@types/ws": "^8.18.1", + "ai": "^6.0.190", + "ajv": "^8.18.0", + "ajv-formats": "^3.0.1", + "async": "^3.2.6", + "binary-extensions": "^3.1.0", + "cache-manager": "^7.2.8", + "chalk": "^5.6.2", + "chokidar": "5.0.0", + "cli-progress": "^3.12.0", + "cli-table3": "^0.6.5", + "commander": "^14.0.3", + "compression": "^1.8.1", + "cors": "^2.8.6", + "csv-parse": "^7.0.0", + "csv-stringify": "^6.7.0", + "debounce": "^3.0.0", + "dedent": "^1.7.2", + "dotenv": "^17.3.1", + "drizzle-orm": "^0.45.1", + "execa": "^9.6.1", + "express": "^5.2.1", + "exsolve": "^1.0.8", + "fast-deep-equal": "^3.1.3", + "fast-safe-stringify": "^2.1.1", + "fast-xml-parser": "^5.7.1", + "fastest-levenshtein": "^1.0.16", + "gcp-metadata": "^8.1.2", + "glob": "^13.0.6", + "http-z": "^8.1.1", + "istextorbinary": "^9.5.0", + "js-rouge": "^3.2.0", + "js-yaml": "5.2.0", + "json5": "^2.2.3", + "keyv": "^5.6.0", + "keyv-file": "^5.3.3", + "lru-cache": "^11.3.0", + "mathjs": "^15.1.1", + "minimatch": "^10.2.4", + "nunjucks": "^3.2.4", + "openai": "^6.37.0", + "opener": "^1.5.2", + "ora": "^9.3.0", + "parse5": "^8.0.0", + "posthog-node": "~5.24.10", + "protobufjs": "^8.0.0", + "proxy-agent": "^8.0.0", + "proxy-from-env": "^2.1.0", + "python-shell": "^5.0.0", + "rfdc": "^1.4.1", + "rxjs": "^7.8.2", + "saxes": "^6.0.0", + "semver": "^7.7.4", + "simple-git": "^3.33.0", + "socket.io": "^4.8.3", + "socket.io-client": "^4.8.3", + "text-extensions": "^3.1.0", + "tsx": "^4.21.0", + "undici": ">=7.28.0 <8", + "winston": "^3.19.0", + "ws": "^8.19.0", + "zod": "^4.3.6" + }, + "bin": { + "pf": "dist/src/entrypoint.js", + "promptfoo": "dist/src/entrypoint.js" + }, + "engines": { + "node": "^20.20.0 || >=22.22.0" + }, + "optionalDependencies": { + "@anthropic-ai/claude-agent-sdk": "0.3.195", + "@aws-sdk/client-bedrock-agent-runtime": "^3.1045.0", + "@aws-sdk/client-bedrock-runtime": "^3.1045.0", + "@aws-sdk/client-s3": "^3.1003.0", + "@aws-sdk/client-sagemaker-runtime": "^3.1045.0", + "@aws-sdk/credential-provider-sso": "^3.972.16", + "@azure/ai-projects": "^2.1.1", + "@azure/identity": "^4.13.0", + "@azure/msal-node": "^5.2.0", + "@azure/openai-assistants": "^1.0.0-beta.6", + "@azure/storage-blob": "^12.31.0", + "@fal-ai/client": "~1.10.1", + "@googleapis/sheets": "^13.0.1", + "@huggingface/transformers": "^4.0.0", + "@ibm-cloud/watsonx-ai": "^1.7.14", + "@ibm-generative-ai/node-sdk": "^3.2.4", + "@modelcontextprotocol/sdk": "^1.29.0", + "@openai/agents": "^0.11.3", + "@openai/codex-sdk": "^0.142.3", + "@opencode-ai/sdk": "^1.14.33", + "@playwright/browser-chromium": "^1.60.0", + "@rollup/rollup-linux-x64-gnu": "^4.62.0", + "@slack/web-api": "^7.15.2", + "@smithy/node-http-handler": "^4.4.14", + "@swc/core": "^1.15.41", + "@swc/core-darwin-arm64": "^1.15.41", + "@swc/core-darwin-x64": "^1.15.41", + "@swc/core-linux-x64-gnu": "^1.15.41", + "@swc/core-linux-x64-musl": "^1.15.41", + "@swc/core-win32-x64-msvc": "^1.15.41", + "google-auth-library": "^10.9.0", + "hono": "^4.12.25", + "ibm-cloud-sdk-core": "^5.4.22", + "jks-js": "^1.1.5", + "langfuse": "^3.38.20", + "natural": "^8.1.1", + "node-sql-parser": "^5.4.0", + "pdf-parse": "^2.4.5", + "pem": "~1.14.8", + "playwright": "^1.60.0", + "playwright-extra": "^4.3.6", + "read-excel-file": "^9.0.0", + "sharp": "^0.35.1" + } + }, + "node_modules/promptfoo/node_modules/@huggingface/jinja": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.9.tgz", + "integrity": "sha512-uWTG+l3VJRsl7EXxYizuL3P+cCPoc3cRqbWWRcQN0FhejRfbdq0RNhCmbY/YDtnTcz9icdLYuLDjsnz4d8JMuw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/promptfoo/node_modules/@huggingface/transformers": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-4.2.0.tgz", + "integrity": "sha512-8BRCoBMH0XsWaEIamuR0LrJGAfftgHAfb2Vrffy0VKlSAE/MnUJ5/h/zTfEP3fDIft+nk7TqB8xXEyABGitBjQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@huggingface/jinja": "^0.5.6", + "@huggingface/tokenizers": "^0.1.3", + "onnxruntime-node": "1.24.3", + "onnxruntime-web": "1.26.0-dev.20260416-b7804b056c", + "sharp": "^0.34.5" + } + }, + "node_modules/promptfoo/node_modules/@huggingface/transformers/node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/promptfoo/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/promptfoo/node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/promptfoo/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/promptfoo/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/promptfoo/node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/promptfoo/node_modules/execa": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", + "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/promptfoo/node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/promptfoo/node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/promptfoo/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/promptfoo/node_modules/js-yaml": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.0.tgz", + "integrity": "sha512-YeLUMlvR4Ou1B119LIaM0r65JvbOBooJDc9yEu0dClb/uSC5P4FrLU8OCCz/HXWvtPoIrR0dRzABTjo1sTN9Bw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.mjs" + } + }, + "node_modules/promptfoo/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/promptfoo/node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, + "node_modules/promptfoo/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/promptfoo/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/promptfoo/node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/promptfoo/node_modules/onnxruntime-common": { + "version": "1.24.3", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.24.3.tgz", + "integrity": "sha512-GeuPZO6U/LBJXvwdaqHbuUmoXiEdeCjWi/EG7Y1HNnDwJYuk6WUbNXpF6luSUY8yASul3cmUlLGrCCL1ZgVXqA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/promptfoo/node_modules/onnxruntime-node": { + "version": "1.24.3", + "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.24.3.tgz", + "integrity": "sha512-JH7+czbc8ALA819vlTgcV+Q214/+VjGeBHDjX81+ZCD0PCVCIFGFNtT0V4sXG/1JXypKPgScQcB3ij/hk3YnTg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "win32", + "darwin", + "linux" + ], + "dependencies": { + "adm-zip": "^0.5.16", + "global-agent": "^3.0.0", + "onnxruntime-common": "1.24.3" + } + }, + "node_modules/promptfoo/node_modules/onnxruntime-web": { + "version": "1.26.0-dev.20260416-b7804b056c", + "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.26.0-dev.20260416-b7804b056c.tgz", + "integrity": "sha512-MD6Ss4GSpQBo6zqoJzyT9LRbKYs7x/JVN23FT24EcEvlqF4VuzPOeH6X38orZPKHQDbprn7K+SBpu0/mj2CQiw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "flatbuffers": "^25.1.24", + "guid-typescript": "^1.0.9", + "long": "^5.2.3", + "onnxruntime-common": "1.24.0-dev.20251116-b39e144322", + "platform": "^1.3.6", + "protobufjs": "^7.2.4" + } + }, + "node_modules/promptfoo/node_modules/onnxruntime-web/node_modules/onnxruntime-common": { + "version": "1.24.0-dev.20251116-b39e144322", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.24.0-dev.20251116-b39e144322.tgz", + "integrity": "sha512-BOoomdHYmNRL5r4iQ4bMvsl2t0/hzVQ3OM3PHD0gxeXu1PmggqBv3puZicEUVOA3AtHHYmqZtjMj9FOfGrATTw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/promptfoo/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/promptfoo/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/promptfoo/node_modules/sharp": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz", + "integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.3", + "@img/sharp-darwin-x64": "0.35.3", + "@img/sharp-freebsd-wasm32": "0.35.3", + "@img/sharp-libvips-darwin-arm64": "1.3.2", + "@img/sharp-libvips-darwin-x64": "1.3.2", + "@img/sharp-libvips-linux-arm": "1.3.2", + "@img/sharp-libvips-linux-arm64": "1.3.2", + "@img/sharp-libvips-linux-ppc64": "1.3.2", + "@img/sharp-libvips-linux-riscv64": "1.3.2", + "@img/sharp-libvips-linux-s390x": "1.3.2", + "@img/sharp-libvips-linux-x64": "1.3.2", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", + "@img/sharp-libvips-linuxmusl-x64": "1.3.2", + "@img/sharp-linux-arm": "0.35.3", + "@img/sharp-linux-arm64": "0.35.3", + "@img/sharp-linux-ppc64": "0.35.3", + "@img/sharp-linux-riscv64": "0.35.3", + "@img/sharp-linux-s390x": "0.35.3", + "@img/sharp-linux-x64": "0.35.3", + "@img/sharp-linuxmusl-arm64": "0.35.3", + "@img/sharp-linuxmusl-x64": "0.35.3", + "@img/sharp-webcontainers-wasm32": "0.35.3", + "@img/sharp-win32-arm64": "0.35.3", + "@img/sharp-win32-ia32": "0.35.3", + "@img/sharp-win32-x64": "0.35.3" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/promptfoo/node_modules/sharp/node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz", + "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.2" + } + }, + "node_modules/promptfoo/node_modules/sharp/node_modules/@img/sharp-darwin-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz", + "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.2" + } + }, + "node_modules/promptfoo/node_modules/sharp/node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz", + "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/promptfoo/node_modules/sharp/node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz", + "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/promptfoo/node_modules/sharp/node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz", + "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/promptfoo/node_modules/sharp/node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz", + "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/promptfoo/node_modules/sharp/node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz", + "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/promptfoo/node_modules/sharp/node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz", + "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/promptfoo/node_modules/sharp/node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz", + "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/promptfoo/node_modules/sharp/node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz", + "integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/promptfoo/node_modules/sharp/node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz", + "integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/promptfoo/node_modules/sharp/node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz", + "integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/promptfoo/node_modules/sharp/node_modules/@img/sharp-linux-arm": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz", + "integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.2" + } + }, + "node_modules/promptfoo/node_modules/sharp/node_modules/@img/sharp-linux-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz", + "integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.2" + } + }, + "node_modules/promptfoo/node_modules/sharp/node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz", + "integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.2" + } + }, + "node_modules/promptfoo/node_modules/sharp/node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz", + "integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.2" + } + }, + "node_modules/promptfoo/node_modules/sharp/node_modules/@img/sharp-linux-s390x": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz", + "integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.2" + } + }, + "node_modules/promptfoo/node_modules/sharp/node_modules/@img/sharp-linux-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz", + "integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.2" + } + }, + "node_modules/promptfoo/node_modules/sharp/node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz", + "integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" + } + }, + "node_modules/promptfoo/node_modules/sharp/node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz", + "integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.2" + } + }, + "node_modules/promptfoo/node_modules/sharp/node_modules/@img/sharp-win32-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz", + "integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/promptfoo/node_modules/sharp/node_modules/@img/sharp-win32-ia32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz", + "integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/promptfoo/node_modules/sharp/node_modules/@img/sharp-win32-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz", + "integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/promptfoo/node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/promptfoo/node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -23267,9 +31663,9 @@ "version": "7.6.4", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz", "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==", + "devOptional": true, "hasInstallScript": true, "license": "BSD-3-Clause", - "optional": true, "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -23291,8 +31687,8 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "license": "Apache-2.0", - "optional": true + "devOptional": true, + "license": "Apache-2.0" }, "node_modules/proxy-addr": { "version": "2.0.7", @@ -23307,6 +31703,26 @@ "node": ">= 0.10" } }, + "node_modules/proxy-agent": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-8.0.2.tgz", + "integrity": "sha512-idLLRewuemWd7GH/BDJzGiB0dWGfT2SQs3jy6NtZtGWU9uPTTSdeC1/cdbqLwgzhfv027daGFuXX426e2Eg20A==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4", + "http-proxy-agent": "9.1.0", + "https-proxy-agent": "9.1.0", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "9.1.0", + "proxy-from-env": "^2.0.0", + "socks-proxy-agent": "10.1.0" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/proxy-agent-negotiate": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-agent-negotiate/-/proxy-agent-negotiate-1.1.0.tgz", @@ -23324,6 +31740,46 @@ } } }, + "node_modules/proxy-agent/node_modules/http-proxy-agent": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-9.1.0.tgz", + "integrity": "sha512-2NxoveTT58mjYT4n3RPTEfCZGLMbidoO8XEieXfpSYxu+PQJ1qpx4ypwH6N+uF9twBPIvRRgvkvW5HUTYWENig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4", + "proxy-agent-negotiate": "1.1.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-agent/node_modules/socks-proxy-agent": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-10.1.0.tgz", + "integrity": "sha512-WlMj/67cEJ6MDI1OcsnjuYKDNDoyPCCYZ249kuuXPiMDw9F8PXkVaQ7YWu3siTydfQ/4BEZcvGzu+aYvz7dDCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/proxy-from-env": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", @@ -23333,6 +31789,20 @@ "node": ">=10" } }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, "node_modules/pug": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pug/-/pug-3.0.4.tgz", @@ -23539,6 +32009,16 @@ "node": ">=16.0.0" } }, + "node_modules/python-shell": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/python-shell/-/python-shell-5.0.0.tgz", + "integrity": "sha512-RUOOOjHLhgR1MIQrCtnEqz/HJ1RMZBIN+REnpSUrfft2bXqXy69fwJASVziWExfFXsR1bCY0TznnHooNsCo0/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, "node_modules/qs": { "version": "6.15.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", @@ -23554,6 +32034,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -23581,6 +32069,26 @@ "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", "license": "MIT" }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/quickjs-wasi": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/quickjs-wasi/-/quickjs-wasi-2.2.0.tgz", + "integrity": "sha512-zQxXmQMrEoD3S+jQdYsloq4qAuaxKFHZj6hHqOYGwB2iQZH+q9e/lf5zQPXCKOk0WJuAjzRFbO4KwHIp2D05Iw==", + "dev": true, + "license": "MIT" + }, "node_modules/railroad-diagrams": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", @@ -23823,12 +32331,28 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/read-excel-file": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/read-excel-file/-/read-excel-file-9.3.1.tgz", + "integrity": "sha512-yzC1vJ/yl3PGJfCDrOI6/rBagF0bRm/CK1NTNXYdomB+13mDB9SFyoRibsDXxDAFrwCANfuRqzuUWDljkkSEuQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "fflate": "^0.8.3", + "saxen": "^11.0.2", + "unzipper-esm": "^0.13.2" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "devOptional": true, "license": "MIT", - "optional": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -23971,6 +32495,24 @@ "node": ">=8" } }, + "node_modules/redis": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/redis/-/redis-5.12.1.tgz", + "integrity": "sha512-LDsoVvb/CpoV9EN3FXvgvSHNJWuCIzl9MiO3ppOevuGLpSGJhwfQjpEwfFJcQvNSddHADDdZaWx0HnmMxRXG7g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@redis/bloom": "5.12.1", + "@redis/client": "5.12.1", + "@redis/json": "5.12.1", + "@redis/search": "5.12.1", + "@redis/time-series": "5.12.1" + }, + "engines": { + "node": ">= 18.19.0" + } + }, "node_modules/redis-errors": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", @@ -24305,6 +32847,14 @@ "node": ">=0.10.0" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/reselect": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz", @@ -24331,6 +32881,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -24350,6 +32907,19 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -24388,6 +32958,20 @@ "node": ">= 4" } }, + "node_modules/retry-axios": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/retry-axios/-/retry-axios-2.6.0.tgz", + "integrity": "sha512-pOLi+Gdll3JekwuFjXO3fTq+L9lzMQGcSq7M5gIjExcl3Gu1hd4XXuf5o3+LuSBsaULQH7DiNbsqPd1chVpQGQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=10.7.0" + }, + "peerDependencies": { + "axios": "*" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -24406,6 +32990,94 @@ "dev": true, "license": "MIT" }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/roarr": { "version": "2.15.4", "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", @@ -24431,6 +33103,14 @@ "license": "BSD-3-Clause", "optional": true }, + "node_modules/robot3": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/robot3/-/robot3-0.4.1.tgz", + "integrity": "sha512-hzjy826lrxzx8eRgv80idkf8ua1JAepRc9Efdtj03N3KNJuznQCPlyCJ7gnUmDFwZCLQjxy567mQVKmdv2BsXQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true + }, "node_modules/robust-predicates": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", @@ -24511,6 +33191,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/run-async": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-4.0.6.tgz", + "integrity": "sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -24575,6 +33265,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "devOptional": true, "funding": [ { "type": "github", @@ -24589,8 +33280,7 @@ "url": "https://feross.org/support" } ], - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/safe-push-apply": { "version": "1.0.0", @@ -24651,6 +33341,27 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/saxen": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/saxen/-/saxen-11.0.2.tgz", + "integrity": "sha512-WDb4gqac8uiJzOdOdVpr9NWh9NrJMm7Brn5GX2Poj+mjE/QTXqYQENr8T/mom54dDDgbd3QjwTg23TRHYiWXRA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 20.12" + } + }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -24725,8 +33436,8 @@ "version": "3.0.5", "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", - "license": "MIT", - "optional": true + "devOptional": true, + "license": "MIT" }, "node_modules/selfsigned": { "version": "5.5.0", @@ -25070,6 +33781,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -25154,6 +33873,24 @@ "simple-concat": "^1.0.0" } }, + "node_modules/simple-git": { + "version": "3.36.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.36.0.tgz", + "integrity": "sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "@simple-git/args-pathspec": "^1.0.3", + "@simple-git/argv-parser": "^1.1.0", + "debug": "^4.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/git-js?sponsor=1" + } + }, "node_modules/size-limit": { "version": "12.1.0", "resolved": "https://registry.npmjs.org/size-limit/-/size-limit-12.1.0.tgz", @@ -25194,6 +33931,19 @@ "node": ">=8" } }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/slice-ansi": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", @@ -25261,6 +34011,113 @@ "node": ">=0.10" } }, + "node_modules/socket.io": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.8", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.8.tgz", + "integrity": "sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "~4.4.1", + "ws": "~8.21.0" + } + }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", + "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/socks": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", @@ -25344,6 +34201,17 @@ "dev": true, "license": "(WTFPL OR MIT)" }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "memory-pager": "^1.0.2" + } + }, "node_modules/spdx-compare": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/spdx-compare/-/spdx-compare-1.0.0.tgz", @@ -25564,6 +34432,16 @@ "dev": true, "license": "MIT" }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -25598,6 +34476,17 @@ "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", "license": "MIT" }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, "node_modules/state-local": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", @@ -25658,12 +34547,23 @@ "node": ">= 0.4" } }, + "node_modules/stopwords-iso": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stopwords-iso/-/stopwords-iso-1.1.0.tgz", + "integrity": "sha512-I6GPS/E0zyieHehMRPQcqkiBMJKGgLta+1hREixhoLPqEA0AlVFiC43dl8uPpmkkeRdDMzYRWFWk5/l9x7nmNg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "devOptional": true, "license": "MIT", - "optional": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -25701,7 +34601,6 @@ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -25716,8 +34615,7 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { "version": "3.0.0", @@ -25725,7 +34623,6 @@ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", - "optional": true, "engines": { "node": ">=8" } @@ -25736,7 +34633,6 @@ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -25893,7 +34789,6 @@ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -25959,6 +34854,40 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.1.tgz", + "integrity": "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "anynum": "^1.0.1" + } + }, + "node_modules/strtok3": { + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/stubborn-fs": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-2.0.0.tgz", @@ -26074,6 +35003,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sylvester": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/sylvester/-/sylvester-0.0.21.tgz", + "integrity": "sha512-yUT0ukFkFEt4nb+NY+n2ag51aS/u9UHXoZw+A4jgD77/jzZsBoSDHuqysrVCBC4CYR4TYvUJq54ONpXgDBH8tA==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.2.6" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -26081,6 +35020,19 @@ "dev": true, "license": "MIT" }, + "node_modules/system-architecture": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/system-architecture/-/system-architecture-0.1.0.tgz", + "integrity": "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tagged-tag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", @@ -26288,6 +35240,42 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/text-extensions": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-3.1.0.tgz", + "integrity": "sha512-anOjtXr8OT5w4vc/2mP4AYTCE0GWc/21icGmaHtBHnI7pN7o01a/oqG9m06/rGzoAsDm/WNzggBpqptuCmRlZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "dev": true, + "license": "MIT" + }, + "node_modules/textextensions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-6.11.0.tgz", + "integrity": "sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "editions": "^6.21.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -26321,6 +35309,13 @@ "node": ">=20" } }, + "node_modules/tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", + "dev": true, + "license": "MIT" + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -26436,6 +35431,26 @@ "dev": true, "license": "MIT" }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/tough-cookie": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", @@ -26502,6 +35517,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/trough": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", @@ -26512,6 +35537,13 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "dev": true, + "license": "MIT" + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -26865,6 +35897,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typed-function": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.2.2.tgz", + "integrity": "sha512-VwaXim9Gp1bngi/q3do8hgttYn2uC3MoT/gfuMWylnj1IeZBUAyPddHZlo1K05BDoj8DYPpMdiHqH1dDYdJf2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, "node_modules/typed-inject": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/typed-inject/-/typed-inject-5.0.0.tgz", @@ -26892,6 +35934,13 @@ "node": ">= 16.0.0" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "dev": true, + "license": "MIT" + }, "node_modules/typescript": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", @@ -26936,6 +35985,20 @@ "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", "license": "MIT" }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unbash": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/unbash/-/unbash-4.0.1.tgz", @@ -27206,6 +36269,21 @@ "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, + "node_modules/unzipper-esm": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/unzipper-esm/-/unzipper-esm-0.13.2.tgz", + "integrity": "sha512-lt8GtgDYV8YcAFZNQuLyR2QvHI8C/TstpgsdjUn9ZxiWLJgn+e5uW6DsO3e/HUJVuWD57ZLLFMZ9xk26tePuHQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "graceful-fs": "^4.2.2", + "node-int64": "^0.4.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -27295,6 +36373,26 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", + "dev": true, + "license": "BSD", + "optional": true + }, "node_modules/use-callback-ref": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", @@ -27460,6 +36558,19 @@ "node": ">= 0.8" } }, + "node_modules/version-range": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/version-range/-/version-range-4.15.0.tgz", + "integrity": "sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg==", + "dev": true, + "license": "Artistic-2.0", + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -27777,6 +36888,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", @@ -27971,6 +37092,54 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/winston": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", + "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.8", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston/node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/with": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz", @@ -27997,6 +37166,68 @@ "node": ">=0.10.0" } }, + "node_modules/wordnet-db": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/wordnet-db/-/wordnet-db-3.1.14.tgz", + "integrity": "sha512-zVyFsvE+mq9MCmwXUWHIcpfbrHHClZWZiVOzKSxNJruIcFn2RbY55zkhiAMMxM8zCVSmtNiViq8FsAZSFpMYag==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/worker-factory": { + "version": "7.0.50", + "resolved": "https://registry.npmjs.org/worker-factory/-/worker-factory-7.0.50.tgz", + "integrity": "sha512-hhwc0G+sFwM4qBuhJIUBn2p1Jf8v/FwmLUANBf/Q+Lt2uI8mfIZQhXaZQACodQD4R7Zp6cn/6702bIvNn2puJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "fast-unique-numbers": "^9.0.27", + "tslib": "^2.8.1" + } + }, + "node_modules/worker-timers": { + "version": "8.0.33", + "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-8.0.33.tgz", + "integrity": "sha512-RQVlKkek80v8M6SHvdMKiywaXFmX3XTpYTXTw0r62PdXB06t74XNxcTuG8wsQwnjorAQymNQyyQ0rzv7PykEMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "tslib": "^2.8.1", + "worker-timers-broker": "^8.0.18", + "worker-timers-worker": "^9.0.15" + } + }, + "node_modules/worker-timers-broker": { + "version": "8.0.18", + "resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-8.0.18.tgz", + "integrity": "sha512-FrjzDVX1wKfZN0gRbCFqv8VHuTncG4sbI/WGEg4tSSQeIsnwqg4YBYWMAHYLJtUDEYYmiK65UKFrVMVk2irDSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "broker-factory": "^3.1.15", + "fast-unique-numbers": "^9.0.27", + "tslib": "^2.8.1", + "worker-timers-worker": "^9.0.15" + } + }, + "node_modules/worker-timers-worker": { + "version": "9.0.15", + "resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-9.0.15.tgz", + "integrity": "sha512-KKUe7lZ/Aignr51H6hOUik8LwTnIgojH/1lwhli8A8qIEIyewogZTpNpMW5B6BF7nmwBOkUoTYgf1H3QShcjSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "tslib": "^2.8.1", + "worker-factory": "^7.0.50" + } + }, "node_modules/wrap-ansi": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-10.0.0.tgz", @@ -28021,7 +37252,6 @@ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -28039,8 +37269,7 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { "version": "3.0.0", @@ -28048,7 +37277,6 @@ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", - "optional": true, "engines": { "node": ">=8" } @@ -28059,7 +37287,6 @@ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -28075,7 +37302,6 @@ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -28202,6 +37428,46 @@ "node": ">=18" } }, + "node_modules/xml-naming": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz", + "integrity": "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/xmlbuilder2": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-4.0.3.tgz", @@ -28248,6 +37514,46 @@ "dev": true, "license": "MIT" }, + "node_modules/xmldom-format": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmldom-format/-/xmldom-format-2.0.0.tgz", + "integrity": "sha512-1zDf0QyGmROs0c/X4ttFMkPeBV+SUYTcLgUj2kpDNKw2g0Ta3dZtFWWYuJR/hQNeUJH6/M/Q9VxOWJRrT0qRug==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@xmldom/xmldom": "^0.9.5" + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/xpath": { + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.34.tgz", + "integrity": "sha512-FxF6+rkr1rNSQrhUNYrAFJpRXNzlDoMxeXN5qI84939ylEv3qqPFKa85Oxr6tDaJKqwW6KKyo2v26TSv3k6LeA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.4" + } + }, "node_modules/xxhash-wasm": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", @@ -28408,6 +37714,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/yoga-layout": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", @@ -28486,7 +37805,7 @@ }, "open-sse": { "name": "@omniroute/open-sse", - "version": "3.8.48", + "version": "3.8.49", "dependencies": { "@toon-format/toon": "^2.3.0", "safe-regex": "^2.1.1" diff --git a/package.json b/package.json index d90aa5b11e..937d70363d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "omniroute", - "version": "3.8.48", + "version": "3.8.49", "description": "Unified AI router with 160+ providers, RTK+Caveman compression, auto fallback, MCP/A2A, desktop, PWA, and OpenAI-compatible APIs.", "type": "module", "bin": { @@ -87,6 +87,7 @@ "build:release": "rm -rf .build dist && OMNIROUTE_BUILD_SHA=$(git rev-parse --short HEAD) npm run build && npm run build:cli && node scripts/build/write-build-sha.mjs", "build:native:tproxy": "cd src/mitm/tproxy/native && npx --yes node-gyp rebuild", "start": "node scripts/dev/run-next.mjs start", + "homolog": "node scripts/homolog/run.mjs", "lint": "eslint . --cache --cache-location .eslintcache --suppressions-location config/quality/eslint-suppressions.json", "lint:json": "node scripts/quality/run-eslint-json.mjs", "lint:md": "npx --yes markdownlint-cli2 \"docs/**/*.md\" \"*.md\" \"!docs/i18n\" \"!docs/research\"", @@ -129,6 +130,7 @@ "i18n:check-ui-coverage": "node scripts/i18n/check-ui-keys-coverage.mjs", "check:node-runtime": "node --import tsx scripts/check/check-supported-node-runtime.ts", "check:pack-artifact": "node --import tsx scripts/build/validate-pack-artifact.ts", + "check:pack-boot": "node scripts/check/check-pack-boot.mjs", "check:pack-policy": "node --import tsx scripts/build/validate-pack-artifact.ts --policy-only", "check:cli-i18n": "node scripts/check/check-cli-i18n.mjs", "check:openapi-coverage": "node scripts/check/check-openapi-coverage.mjs", @@ -183,6 +185,7 @@ "audit:electron": "npm --prefix electron audit --audit-level=critical && (npm --prefix electron audit --audit-level=high || echo '::warning::electron high-severity advisories present (non-blocking)')", "typecheck:core": "tsc --pretty false -p tsconfig.typecheck-core.json", "typecheck:noimplicit:core": "tsc --pretty false -p tsconfig.typecheck-noimplicit-core.json", + "check:dashboard-typecheck": "node scripts/check/check-dashboard-typecheck.mjs", "backfill-aggregation": "node --import tsx src/scripts/backfillAggregation.ts", "env:sync": "node scripts/dev/sync-env.mjs", "test:integration": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=1 tests/integration/*.test.ts \"tests/integration/combo-matrix/*.test.ts\"", @@ -328,21 +331,26 @@ "c8": "^11.0.0", "concurrently": "^10.0.3", "cross-env": "^10.1.0", + "ctrf": "^0.2.1", "dpdm": "^4.2.0", "eslint": "^9.39.4", "eslint-config-next": "16.2.10", "eslint-plugin-sonarjs": "^4.1.0", "fast-check": "^4.8.0", "glob": "^13.0.6", + "httpyac": "^6.16.7", "husky": "^9.1.7", "jscpd": "^4.2.5", "jsdom": "^29.1.1", + "junit-to-ctrf": "^0.0.14", "knip": "^6.18.0", "license-checker-rseidelsohn": "^5.0.1", "lint-staged": "^17.0.8", "lockfile-lint": "^5.0.0", "node-loader": "^2.1.0", + "playwright-ctrf-json-reporter": "^0.0.29", "prettier": "^3.8.3", + "promptfoo": "^0.121.18", "size-limit": "^12.1.0", "tailwindcss": "^4.3.0", "type-coverage": "^2.29.7", diff --git a/scripts/build/assembleStandalone.mjs b/scripts/build/assembleStandalone.mjs index b0d8892b27..bdaed25aea 100644 --- a/scripts/build/assembleStandalone.mjs +++ b/scripts/build/assembleStandalone.mjs @@ -39,7 +39,8 @@ * prune + validate (pack-artifact-policy) - Y - UNIQUE (prepublish) * data/ dir creation - Y - UNIQUE (prepublish) * --- electron-UNIQUE --- - * better-sqlite3 + keytar native strip (ABI rebuild) - - Y UNIQUE (electron) + * better-sqlite3 native strip + Electron-ABI rebuild - - Y UNIQUE (electron) + * Turbopack hashed-module symlink materialize (node_modules) - - Y SHARED (opt-in: materializeSymlinks) * symlink guard (assertBundleIsPackagable) - - Y UNIQUE (electron) * removeGeneratedElectronArtifacts - - Y UNIQUE (electron) */ @@ -135,6 +136,11 @@ const EXTRA_MODULE_ENTRIES = [ src: ["scripts", "dev", "peer-stamp.mjs"], dest: ["peer-stamp.mjs"], }, + { + label: "main-server timeouts (server-ws.mjs dependency, #7003/#7065-class)", + src: ["scripts", "dev", "main-server-timeouts.mjs"], + dest: ["main-server-timeouts.mjs"], + }, { label: "HTTP method guard (server-ws.mjs dependency)", src: ["scripts", "dev", "http-method-guard.cjs"], @@ -464,6 +470,140 @@ function copyNativeAssetsAndExtraModules(projectRoot, resolvedOutDir) { } } +/** + * Materialize Turbopack "hashed external module" symlinks inside a bundled + * node_modules dir into real, self-contained directories. + * + * Next.js/Turbopack standalone output emits entries like + * better-sqlite3-90e2652d1716b047 -> /node_modules/better-sqlite3 + * as ABSOLUTE symlinks into the build machine's tree. cpSync preserves symlinks and + * electron-builder preserves extraResources symlinks verbatim, so the packaged app + * ships dangling links pointing at e.g. /Users/runner/work/... On the end-user machine + * those targets don't exist → the instrumentation hook throws + * ERR_MODULE_NOT_FOUND: Cannot find package 'ws-' → server boot fails. + * (issues #6724, #6594). Windows is doubly broken because it can't follow POSIX + * symlinks at all. + * + * The fix: for every symlink under the given node_modules (top level + one level of + * scoped @scope/ dirs), replace it with a REAL directory copy of its dereferenced + * target — a dereference is the only option that is correct on every OS (Windows + * included) and survives the machine that built it. If the link is already dangling + * (target absent), fall back to copying a sibling real package whose name is the + * hashed name with its trailing `-` suffix stripped; if none exists, drop the + * dangling link so it cannot poison module resolution. + * + * @param {string} nodeModulesDir - absolute path to a bundled node_modules directory + * @returns {{ materialized: number, relinked: number, removed: number }} + */ +export function materializeBundledSymlinks(nodeModulesDir) { + const summary = { materialized: 0, relinked: 0, removed: 0 }; + if (!fsSync.existsSync(nodeModulesDir)) return summary; + + const entries = []; + for (const name of fsSync.readdirSync(nodeModulesDir)) { + const entryPath = path.join(nodeModulesDir, name); + if (name.startsWith("@") && fsSync.lstatSync(entryPath).isDirectory()) { + // Scoped packages live one level deeper (@scope/pkg). + for (const scoped of fsSync.readdirSync(entryPath)) { + entries.push(path.join(entryPath, scoped)); + } + continue; + } + entries.push(entryPath); + } + + for (const entryPath of entries) { + let stat; + try { + stat = fsSync.lstatSync(entryPath); + } catch { + continue; + } + if (!stat.isSymbolicLink()) continue; + + let realTarget = null; + try { + realTarget = fsSync.realpathSync(entryPath); + } catch { + realTarget = null; + } + + if (realTarget && fsSync.existsSync(realTarget)) { + // Dereference: copy the resolved real files in place of the link. + fsSync.rmSync(entryPath, { recursive: true, force: true }); + fsSync.cpSync(realTarget, entryPath, { recursive: true, dereference: true }); + summary.materialized += 1; + continue; + } + + // Dangling link (e.g. absolute path into the build machine that no longer + // exists). Try a sibling real package named without the trailing - hash. + const baseName = path.basename(entryPath).replace(/-[0-9a-f]{8,}$/i, ""); + const sibling = path.join(path.dirname(entryPath), baseName); + if (baseName !== path.basename(entryPath) && fsSync.existsSync(sibling)) { + let siblingStat = null; + try { + siblingStat = fsSync.lstatSync(sibling); + } catch { + siblingStat = null; + } + if (siblingStat && siblingStat.isDirectory()) { + fsSync.rmSync(entryPath, { recursive: true, force: true }); + fsSync.cpSync(sibling, entryPath, { recursive: true, dereference: true }); + summary.relinked += 1; + continue; + } + } + + // Nothing to resolve to — drop the dangling link so it cannot shadow resolution. + console.warn( + `[assembleStandalone] Dropping dangling module symlink (target missing): ${entryPath}` + ); + fsSync.rmSync(entryPath, { recursive: true, force: true }); + summary.removed += 1; + } + + return summary; +} + +/** + * Sync an Electron-ABI-rebuilt native module into any hashed/plain copies of + * that module already materialized inside a nested node_modules dir. + * + * materializeBundledSymlinks() turns Turbopack hashed-module symlinks (e.g. + * `better-sqlite3-90e2652d1716b047`) into real directory copies of the + * Node-ABI build. A later step in prepare-electron-standalone.mjs rebuilds + * better-sqlite3 against the Electron ABI at the bundle root — but the + * hashed copy under the nested node_modules still holds the stale Node-ABI + * build, and the server's hashed `require("better-sqlite3-")` resolves + * to it, not the rebuilt root module. Previously that hashed copy was simply + * deleted, which caused MODULE_NOT_FOUND and a silent fallback to the sql.js + * WASM driver in the packaged app (issue #6794 follow-up). Overwriting each + * matching entry with the rebuilt root module keeps the hashed require + * resolving to a working, ABI-correct native driver instead. + * + * @param {string} rootModuleDir - absolute path to the already-rebuilt module (e.g. /node_modules/better-sqlite3) + * @param {string} nodeModulesDir - absolute path to the nested node_modules dir to scan + * @returns {{ synced: number }} + */ +export function syncRebuiltNativeModuleIntoHashedEntries(rootModuleDir, nodeModulesDir) { + const summary = { synced: 0 }; + if (!fsSync.existsSync(rootModuleDir) || !fsSync.existsSync(nodeModulesDir)) return summary; + + const baseName = path.basename(rootModuleDir); + const pattern = new RegExp(`^${baseName}(-[0-9a-f]{8,})?$`, "i"); + + for (const name of fsSync.readdirSync(nodeModulesDir)) { + if (!pattern.test(name)) continue; + const entryPath = path.join(nodeModulesDir, name); + fsSync.rmSync(entryPath, { recursive: true, force: true }); + fsSync.cpSync(rootModuleDir, entryPath, { recursive: true, dereference: true }); + summary.synced += 1; + } + + return summary; +} + /** * Assemble the Next.js standalone bundle into outDir. * @@ -480,6 +620,7 @@ function copyNativeAssetsAndExtraModules(projectRoot, resolvedOutDir) { * @param {boolean} [opts.sanitizePaths] - replace build-machine abs paths with "." (default false) * @param {boolean} [opts.patchTurbopackChunks] - strip hashed externals from .next/server js files (default false) * @param {boolean} [opts.copyNatives] - copy native assets + extra modules (default true) + * @param {boolean} [opts.materializeSymlinks] - dereference Turbopack hashed-module symlinks in node_modules (default false) * @returns {void} */ export function assembleStandalone({ @@ -489,6 +630,7 @@ export function assembleStandalone({ sanitizePaths = false, patchTurbopackChunks: doPatchChunks = false, copyNatives = true, + materializeSymlinks = false, }) { if (!distDir) throw new Error("[assembleStandalone] distDir is required"); if (!outDir) throw new Error("[assembleStandalone] outDir is required"); @@ -545,4 +687,23 @@ export function assembleStandalone({ if (copyNatives) { copyNativeAssetsAndExtraModules(projectRoot, resolvedOutDir); } + + // 7. Optionally dereference Turbopack hashed-module symlinks so the bundle is + // self-contained (no absolute links into the build machine). Runs AFTER the + // native/extra-module copy so the sibling-package relink fallback can find + // real packages. See materializeBundledSymlinks + issues #6724, #6594. + if (materializeSymlinks) { + for (const nmDir of [ + path.join(resolvedOutDir, "node_modules"), + path.join(resolvedOutDir, relDistDir, "node_modules"), + ]) { + const s = materializeBundledSymlinks(nmDir); + if (s.materialized || s.relinked || s.removed) { + console.log( + `[assembleStandalone] Materialized module symlinks in ${path.relative(resolvedOutDir, nmDir) || "."}: ` + + `${s.materialized} dereferenced, ${s.relinked} relinked, ${s.removed} dropped` + ); + } + } + } } diff --git a/scripts/build/pack-artifact-policy.ts b/scripts/build/pack-artifact-policy.ts index 88329052a1..cc60babe71 100644 --- a/scripts/build/pack-artifact-policy.ts +++ b/scripts/build/pack-artifact-policy.ts @@ -46,6 +46,7 @@ export const APP_STAGING_ALLOWED_EXACT_PATHS: string[] = [ "open-sse/services/compression/engines/llmlingua/onnxWorker.js", "package.json", "peer-stamp.mjs", + "main-server-timeouts.mjs", "responses-ws-proxy.mjs", "scripts/dev/sync-env.mjs", "scripts/dev/tls-options.mjs", @@ -152,6 +153,7 @@ export const PACK_ARTIFACT_REQUIRED_PATHS: string[] = [ "dist/server-ws.mjs", "dist/responses-ws-proxy.mjs", "dist/peer-stamp.mjs", + "dist/main-server-timeouts.mjs", "dist/http-method-guard.cjs", // #5452: regression guard — make check:pack-artifact fail loudly if the TLS // opt-in sidecar (imported by dist/server-ws.mjs) ever vanishes from the tarball. @@ -160,6 +162,12 @@ export const PACK_ARTIFACT_REQUIRED_PATHS: string[] = [ "dist/head-response-guard.cjs", "dist/webdav-handler.mjs", "bin/cli/program.mjs", + // Direct imports of bin/omniroute.mjs — bin/cli/ is only an allowlist PREFIX, so a + // file vanishing from the tarball never fails the unexpected-paths check; only these + // required entries make its absence loud (#7065 class; derived + enforced by + // tests/unit/pack-artifact-entrypoint-closures.test.ts). + "bin/cli/data-dir.mjs", + "bin/cli/utils/storageKeyProvision.mjs", "bin/mcp-server.mjs", "bin/nodeRuntimeSupport.mjs", "bin/omniroute.mjs", diff --git a/scripts/build/prepare-electron-standalone.mjs b/scripts/build/prepare-electron-standalone.mjs index f48e1556a8..da04fca00b 100644 --- a/scripts/build/prepare-electron-standalone.mjs +++ b/scripts/build/prepare-electron-standalone.mjs @@ -178,6 +178,9 @@ assembleStandalone({ projectRoot: ROOT, sanitizePaths: true, copyNatives: true, + // #6724/#6594: dereference Turbopack hashed-module symlinks — inside the packaged + // app they would point at the build machine's absolute paths and break on install. + materializeSymlinks: true, }); // Electron-UNIQUE post-assembly steps diff --git a/scripts/check/check-dashboard-typecheck.mjs b/scripts/check/check-dashboard-typecheck.mjs new file mode 100644 index 0000000000..454bb308e9 --- /dev/null +++ b/scripts/check/check-dashboard-typecheck.mjs @@ -0,0 +1,177 @@ +#!/usr/bin/env node +// scripts/check/check-dashboard-typecheck.mjs +// Dashboard-scoped typecheck gate (#7033). +// +// `typecheck:core` (the only blocking CI typecheck gate) runs against a curated +// 27-file `"files"` allowlist in tsconfig.typecheck-core.json — none of it lives +// under `src/app/(dashboard)`, and `next.config.mjs` sets +// `typescript.ignoreBuildErrors: true`, so `next build` never type-checks either. +// Net effect: orphaned-identifier regressions in dashboard TSX (deleted `useState` +// decls with live usages left behind) are invisible to both CI type-check paths +// and only surface at runtime — exactly what happened in #6625/#6909. +// +// This gate runs `tsc` scoped to `src/app/(dashboard)/**/*.{ts,tsx}` via +// tsconfig.typecheck-dashboard.json and diffs the result against a frozen +// per-file/per-TS-code count baseline (config/quality/dashboard-typecheck-baseline.json), +// following this repo's stale-enforcement allowlist convention (see +// scripts/check/check-known-symbols.ts). A live count that EXCEEDS the baselined +// count for a given (file, TS code) pair is a regression and fails the gate; a +// live count that is lower is an improvement and does not fail (use --update to +// ratchet the baseline down). +// +// Run: +// node scripts/check/check-dashboard-typecheck.mjs +// node scripts/check/check-dashboard-typecheck.mjs --update # re-freeze baseline + +import { execFileSync } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; + +const ROOT = process.cwd(); +const TSCONFIG = path.join(ROOT, "tsconfig.typecheck-dashboard.json"); +const BASELINE_PATH = path.join(ROOT, "config/quality/dashboard-typecheck-baseline.json"); +const UPDATE = process.argv.includes("--update"); + +// Matches tsc --pretty false output lines, e.g.: +// src/app/(dashboard)/dashboard/foo.tsx(12,7): error TS2304: Cannot find name 'bar'. +const TSC_ERROR_LINE = /^(.+?)\((\d+),(\d+)\): error (TS\d+):/; + +/** + * Parses raw `tsc --pretty false` stdout into a nested count map: + * { "": { "": } } + * + * Pure/exported for unit testing against synthetic tsc output — no child + * process involved here. + */ +export function parseTscOutput(raw) { + const counts = {}; + const lines = String(raw).split("\n"); + for (const line of lines) { + const match = TSC_ERROR_LINE.exec(line); + if (!match) continue; + const [, file, , , code] = match; + if (!counts[file]) counts[file] = {}; + counts[file][code] = (counts[file][code] || 0) + 1; + } + return counts; +} + +/** + * Compares live (file, TS code) error counts against a frozen baseline. + * Returns `{ regressions, improvements }`: + * - regressions: entries where live count > baselined count (or the pair is + * entirely new/unbaselined) — these fail the gate. + * - improvements: entries where live count < baselined count — informational, + * do not fail (use --update to ratchet the baseline down). + * + * Exported for unit testing. + */ +export function diffAgainstBaseline(live, baseline) { + const regressions = []; + const improvements = []; + + for (const [file, codes] of Object.entries(live)) { + for (const [code, liveCount] of Object.entries(codes)) { + const baselineCount = (baseline[file] && baseline[file][code]) || 0; + if (liveCount > baselineCount) { + regressions.push({ file, code, liveCount, baselineCount }); + } else if (liveCount < baselineCount) { + improvements.push({ file, code, liveCount, baselineCount }); + } + } + } + + for (const [file, codes] of Object.entries(baseline)) { + for (const [code, baselineCount] of Object.entries(codes)) { + const liveCount = (live[file] && live[file][code]) || 0; + if (liveCount === 0 && baselineCount > 0) { + improvements.push({ file, code, liveCount: 0, baselineCount }); + } + } + } + + return { regressions, improvements }; +} + +function runTsc() { + try { + const stdout = execFileSync( + process.platform === "win32" ? "npx.cmd" : "npx", + ["tsc", "--pretty", "false", "--noEmit", "-p", TSCONFIG], + { encoding: "utf8", maxBuffer: 64 * 1024 * 1024, cwd: ROOT } + ); + return stdout; + } catch (err) { + // tsc exits non-zero when there are type errors — stdout still has the report. + if (err.stdout) return String(err.stdout); + throw err; + } +} + +function loadBaseline() { + if (!fs.existsSync(BASELINE_PATH)) return {}; + return JSON.parse(fs.readFileSync(BASELINE_PATH, "utf8")); +} + +function writeBaseline(counts) { + fs.writeFileSync(BASELINE_PATH, JSON.stringify(counts, null, 2) + "\n"); +} + +function main() { + if (!fs.existsSync(TSCONFIG)) { + process.stderr.write(`[dashboard-typecheck] FAIL — tsconfig not found at ${TSCONFIG}\n`); + process.exit(2); + } + + console.log("[dashboard-typecheck] Running tsc scoped to src/app/(dashboard)/**…"); + const stdout = runTsc(); + const live = parseTscOutput(stdout); + const baseline = loadBaseline(); + const { regressions, improvements } = diffAgainstBaseline(live, baseline); + + const liveErrorCount = Object.values(live).reduce( + (sum, codes) => sum + Object.values(codes).reduce((s, c) => s + c, 0), + 0 + ); + console.log(`dashboardTypecheckErrors=${liveErrorCount}`); + + if (UPDATE) { + writeBaseline(live); + console.log(`[dashboard-typecheck] baseline rewritten (${liveErrorCount} errors frozen).`); + process.exit(0); + } + + if (improvements.length > 0) { + console.log( + `[dashboard-typecheck] ${improvements.length} baselined error(s) no longer present ` + + `— run 'node scripts/check/check-dashboard-typecheck.mjs --update' to ratchet the baseline down:\n` + + improvements + .map((i) => ` - ${i.file} ${i.code} (baseline ${i.baselineCount} -> live ${i.liveCount})`) + .join("\n") + ); + } + + if (regressions.length > 0) { + process.stderr.write( + `[dashboard-typecheck] FAIL — ${regressions.length} new/regressed TypeScript error(s) ` + + `under src/app/(dashboard)/ not covered by the frozen baseline:\n` + + regressions + .map((r) => ` ✗ ${r.file} ${r.code} (baseline ${r.baselineCount}, live ${r.liveCount})`) + .join("\n") + + `\n\nIf this is a genuine new dashboard TSX bug (e.g. an orphaned identifier), fix it.\n` + + `If it's pre-existing type looseness you're intentionally not fixing in this PR,\n` + + `do NOT widen the baseline for new regressions — that defeats the gate.\n` + ); + process.exit(1); + } + + console.log( + `[dashboard-typecheck] OK — ${liveErrorCount} pre-existing error(s), all within frozen baseline.` + ); + process.exit(0); +} + +if (import.meta.url === pathToFileURL(process.argv[1] || "").href) { + main(); +} diff --git a/scripts/check/check-env-doc-sync.mjs b/scripts/check/check-env-doc-sync.mjs index 197d8ddeb9..408c53d5ca 100644 --- a/scripts/check/check-env-doc-sync.mjs +++ b/scripts/check/check-env-doc-sync.mjs @@ -101,6 +101,15 @@ const IGNORE_FROM_CODE = new Set([ // ("http://192.168.0.15:20128" / null), never OmniRoute runtime config (#5151). "COMBO_LIVE_BASE_URL", "COMBO_LIVE_API_KEY", + // Homologation E2E suite (npm run homolog) vars — configured via the dedicated + // .env.homolog file (template: .env.homolog.example), never in the runtime .env. + // Test/ops-only signals against the homologation VPS, same class as COMBO_LIVE_*. + // See docs/ops/HOMOLOGATION.md. + "HOMOLOG_BASE_URL", + "HOMOLOG_ADMIN_PASSWORD", + "HOMOLOG_API_KEY", + "HOMOLOG_CRITICAL_PROVIDERS", + "HOMOLOG_EXPECT_VERSION", // update-notifier opt-out for the CLI binary. "OMNIROUTE_NO_UPDATE_NOTIFIER", // Headless CLI execution flag for Electron. @@ -132,6 +141,12 @@ const IGNORE_FROM_CODE = new Set([ "QA_LOCALES", "QA_REPORT_SUFFIX", "QA_ROUTES", + // Post-publish verifier (scripts/release/verify-published.mjs): env passed INTO the + // clean Docker container script (Hard Rule #13 env-option pattern) — release tooling + // internals, never OmniRoute runtime config. + "VERIFY_DEADLINE_S", + "VERIFY_PORT", + "VERIFY_VERSION", // Doctor diagnostic flags (no runtime behavior yet — placeholders). "OMNIROUTE_DOCTOR_HOST", "OMNIROUTE_DOCTOR_LIVENESS_URL", diff --git a/scripts/check/check-pack-boot.mjs b/scripts/check/check-pack-boot.mjs new file mode 100644 index 0000000000..62a4b78ab5 --- /dev/null +++ b/scripts/check/check-pack-boot.mjs @@ -0,0 +1,166 @@ +#!/usr/bin/env node +/** + * check:pack-boot — boot-smoke of the REAL npm tarball (#7065 class killer, WS1.2/T1). + * + * Three releases shipped a tarball that crashed on every boot (tls-options/3.8.41, + * head-response-guard VPS #7040 + npm #7065) because no gate ever EXECUTED the + * artifact: structure checks (check:pack-artifact) validate lists, not runtime. + * This gate packs the tree, installs the tarball into a clean prefix, boots the + * installed CLI and polls /api/monitoring/health until it proves the artifact + * starts — regardless of WHICH packaging list drifted. + * + * Requires a built dist/ (run after `npm run build:cli`, e.g. in the CI + * package-artifact job or `check:release-green --with-build`). Exit codes: + * 0 = boots and reports the right version · 1 = boot failed · 2 = missing build. + */ +import { execFileSync, spawn } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const POLL_INTERVAL_MS = 2_000; +const BOOT_DEADLINE_MS = 240_000; + +/** Parse `npm pack --json` output into the generated tarball filename. */ +export function pickTarball(packJsonOutput) { + const parsed = JSON.parse(packJsonOutput); + const filename = Array.isArray(parsed) ? parsed[0]?.filename : undefined; + if (!filename) throw new Error("npm pack --json returned no filename"); + // npm >=9 may emit scoped names with "/" — normalize to the on-disk file name. + return filename.replace(/\//g, "-"); +} + +/** + * Boot verdict: HTTP 200 + a JSON body reporting the version we just packed. + * `status` is logged but NOT asserted — a clean install with zero providers may + * legitimately report degraded states; the gate targets boot crashes, not health. + */ +export function evaluateBoot(httpStatus, body, expectedVersion) { + const failures = []; + if (httpStatus !== 200) failures.push(`health HTTP ${httpStatus} (expected 200)`); + if (!body || typeof body !== "object") failures.push("health body is not JSON"); + else if (body.version !== expectedVersion) + failures.push(`version "${body.version}" (expected "${expectedVersion}")`); + return { ok: failures.length === 0, failures }; +} + +/** Deterministic-enough free-ish port in a range CI runners don't use. */ +export function pickPort(seed = process.pid) { + return 23000 + (seed % 4000); +} + +function log(msg) { + console.log(`[pack-boot] ${msg}`); +} + +async function main() { + const ROOT = process.cwd(); + if (!fs.existsSync(path.join(ROOT, "dist", "server.js"))) { + console.error("[pack-boot] dist/server.js missing — run `npm run build:cli` first (this is a --with-build gate)"); + process.exit(2); + } + const expectedVersion = JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf8")).version; + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-pack-boot-")); + let child = null; + let exitCode = 1; + try { + log(`packing v${expectedVersion}…`); + const packOut = execFileSync("npm", ["pack", "--json", "--pack-destination", tmp], { + cwd: ROOT, + encoding: "utf8", + maxBuffer: 64 * 1024 * 1024, + }); + const tarball = path.join(tmp, pickTarball(packOut)); + log(`installing ${path.basename(tarball)} into a clean prefix (postinstall runs for real)…`); + const prefix = path.join(tmp, "prefix"); + execFileSync("npm", ["install", "-g", "--prefix", prefix, tarball], { + encoding: "utf8", + maxBuffer: 64 * 1024 * 1024, + }); + + const port = pickPort(); + const dataDir = path.join(tmp, "data"); + fs.mkdirSync(dataDir, { recursive: true }); + const binPath = path.join(prefix, "bin", "omniroute"); + log(`booting installed CLI on :${port} (DATA_DIR isolated)…`); + child = spawn(binPath, ["serve", "--port", String(port)], { + env: { + ...process.env, + PORT: String(port), + DATA_DIR: dataDir, + JWT_SECRET: "pack-boot-smoke-secret-with-sufficient-length-000", + API_KEY_SECRET: "pack-boot-smoke-api-key-secret-long", + DISABLE_SQLITE_AUTO_BACKUP: "true", + OMNIROUTE_SKIP_SYSTEM_TRUST: "1", + }, + stdio: ["ignore", "pipe", "pipe"], + detached: true, + }); + const tail = []; + const keepTail = (chunk) => { + tail.push(String(chunk)); + while (tail.length > 80) tail.shift(); + }; + child.stdout.on("data", keepTail); + child.stderr.on("data", keepTail); + let childExit = null; + child.on("exit", (code) => { + childExit = code ?? -1; + }); + + const deadline = Date.now() + BOOT_DEADLINE_MS; + let verdict = { ok: false, failures: ["never polled"] }; + while (Date.now() < deadline) { + if (childExit !== null) { + verdict = { ok: false, failures: [`process exited with code ${childExit} before serving`] }; + break; + } + try { + const res = await fetch(`http://127.0.0.1:${port}/api/monitoring/health`); + const body = await res.json().catch(() => null); + verdict = evaluateBoot(res.status, body, expectedVersion); + if (verdict.ok) { + log(`healthy: HTTP 200, version ${body.version}, status "${body.status}"`); + break; + } + } catch { + // not listening yet — keep polling + } + await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS)); + } + + if (verdict.ok) { + log("✅ the packed tarball boots — #7065 class gate green"); + exitCode = 0; + } else { + console.error(`[pack-boot] ❌ boot FAILED: ${verdict.failures.join("; ")}`); + console.error("[pack-boot] last server output:\n" + tail.join("").split("\n").slice(-40).join("\n")); + exitCode = 1; + } + } finally { + if (child?.pid) { + try { + process.kill(-child.pid, "SIGTERM"); + } catch { + /* already gone */ + } + await new Promise((r) => setTimeout(r, 2_000)); + try { + process.kill(-child.pid, "SIGKILL"); + } catch { + /* already gone */ + } + } + fs.rmSync(tmp, { recursive: true, force: true }); + } + process.exit(exitCode); +} + +const isDirectRun = + process.argv[1] && path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname); +if (isDirectRun) { + main().catch((e) => { + console.error("[pack-boot] fatal:", e.message); + process.exit(1); + }); +} diff --git a/scripts/check/check-test-discovery.mjs b/scripts/check/check-test-discovery.mjs index 33a2598ef8..e695ff252c 100644 --- a/scripts/check/check-test-discovery.mjs +++ b/scripts/check/check-test-discovery.mjs @@ -127,6 +127,13 @@ export const COLLECTORS = [ glob: "tests/e2e/protocol-clients.test.ts", sources: ["scripts/dev/run-protocol-clients-tests.mjs"], }, + // Playwright — suíte de homologação real (npm run homolog, L4 UI): run.mjs invoca + // `playwright test -c tests/homolog/ui/playwright.config.ts` (testMatch **/*.spec.ts). + { + glob: "tests/homolog/ui/*.spec.ts", + sources: ["scripts/homolog/run.mjs"], + anchors: { "scripts/homolog/run.mjs": "tests/homolog/ui/playwright.config.ts" }, + }, ]; const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); diff --git a/scripts/dev/main-server-timeouts.mjs b/scripts/dev/main-server-timeouts.mjs new file mode 100644 index 0000000000..a0019fca2d --- /dev/null +++ b/scripts/dev/main-server-timeouts.mjs @@ -0,0 +1,47 @@ +// Main-server keepAlive/headers timeouts (#7003) — SIBLING module of +// standalone-server-ws.mjs. The shipped server-ws.mjs may only import +// siblings copied next to it by assembleStandalone (peer-stamp, tls-options, +// the guards): a ../../src/... import resolves OUTSIDE the package after the +// copy to the dist root and crashes boot with ERR_MODULE_NOT_FOUND (caught +// live by check:pack-boot on 2026-07-15 — the #7065 class). +// Parity with src/shared/utils/runtimeTimeouts.ts#getMainServerTimeoutConfig +// is enforced by tests/unit/main-server-timeouts-parity.test.ts. + +export const DEFAULT_MAIN_SERVER_KEEPALIVE_TIMEOUT_MS = 65_000; +export const DEFAULT_MAIN_SERVER_HEADERS_TIMEOUT_MS = 66_000; + +function readTimeoutMs(env, name, defaultValue, { allowZero = false, logger } = {}) { + const raw = env[name]; + if (raw == null || raw.trim() === "") return defaultValue; + const parsed = Number(raw); + const isValid = Number.isFinite(parsed) && (allowZero ? parsed >= 0 : parsed > 0); + if (!isValid) { + logger?.(`Invalid ${name}="${raw}". Using default ${defaultValue}ms.`); + return defaultValue; + } + return Math.floor(parsed); +} + +export function getMainServerTimeoutConfig(env = process.env, logger) { + const keepAliveTimeoutMs = readTimeoutMs( + env, + "MAIN_SERVER_KEEPALIVE_TIMEOUT_MS", + DEFAULT_MAIN_SERVER_KEEPALIVE_TIMEOUT_MS, + { allowZero: true, logger } + ); + const headersTimeoutMs = readTimeoutMs( + env, + "MAIN_SERVER_HEADERS_TIMEOUT_MS", + DEFAULT_MAIN_SERVER_HEADERS_TIMEOUT_MS, + { allowZero: true, logger } + ); + return { + keepAliveTimeoutMs, + // Node requires headersTimeout > keepAliveTimeout; keep both configurable + // but always coherent (mirrors the canonical TS implementation). + headersTimeoutMs: + headersTimeoutMs > 0 && keepAliveTimeoutMs > 0 + ? Math.max(headersTimeoutMs, keepAliveTimeoutMs + 1_000) + : headersTimeoutMs, + }; +} diff --git a/scripts/dev/run-next.mjs b/scripts/dev/run-next.mjs index 68eb9d2198..54c33e56df 100644 --- a/scripts/dev/run-next.mjs +++ b/scripts/dev/run-next.mjs @@ -14,6 +14,7 @@ import headResponseGuard from "./head-response-guard.cjs"; import { ensureNativeSqlite } from "./ensure-native-sqlite.mjs"; import { isTurbopackCacheCorruption, purgeAllTurbopackCaches } from "./turbopackCacheHeal.mjs"; import { randomUUID } from "node:crypto"; +import { getMainServerTimeoutConfig } from "./main-server-timeouts.mjs"; const { maybeHandleDisallowedMethod } = methodGuard; const { wrapRequestListenerWithHeadResponseGuard } = headResponseGuard; @@ -153,6 +154,15 @@ async function start() { return requestHandler(req, res); }) ); + // Node's http.Server default keepAliveTimeout (5_000ms) races pooled + // keep-alive HTTP clients that idle longer than that between requests (e.g. + // the JVM java.net.http.HttpClient used by JetBrains AI Assistant), which + // reuse a socket the server already tore down and get 0 response bytes back + // (#7003). Raise both timeouts well above any realistic client idle-pool + // window, mirroring src/lib/apiBridgeServer.ts's pattern. + const mainServerTimeouts = getMainServerTimeoutConfig(); + server.keepAliveTimeout = mainServerTimeouts.keepAliveTimeoutMs; + server.headersTimeout = mainServerTimeouts.headersTimeoutMs; server.on("upgrade", async (req, socket, head) => { try { const responsesWsHandled = await responsesWsProxy.handleUpgrade(req, socket, head); diff --git a/scripts/dev/standalone-server-ws.mjs b/scripts/dev/standalone-server-ws.mjs index 3d7bd1f0ca..439a9c5171 100644 --- a/scripts/dev/standalone-server-ws.mjs +++ b/scripts/dev/standalone-server-ws.mjs @@ -7,6 +7,7 @@ import { maybeHandleWebdav } from "./webdav-handler.mjs"; import methodGuard from "./http-method-guard.cjs"; import headResponseGuard from "./head-response-guard.cjs"; import { resolveTlsOptions, createServerListener } from "./tls-options.mjs"; +import { getMainServerTimeoutConfig } from "./main-server-timeouts.mjs"; const originalCreateServer = http.createServer.bind(http); const proxiesByPort = new Map(); @@ -151,6 +152,19 @@ http.createServer = function createServerWithResponsesWs(...args) { // listener); otherwise the original http.Server. The downstream .on/.addListener // patches below apply identically to both (https.Server extends http.Server). const server = createServerListener(args, tlsOptions, { createHttp: originalCreateServer }); + // Node's http.Server default keepAliveTimeout (5_000ms) races pooled + // keep-alive HTTP clients that idle longer than that between requests (e.g. + // the JVM java.net.http.HttpClient used by JetBrains AI Assistant), which + // reuse a socket the server already tore down and get 0 response bytes back + // (#7003). This wrapper is what `omniroute serve` / Docker / Electron actually + // spawn in production (run-standalone.mjs prefers server-ws.mjs over the bare + // Next server.js), so it needs the same fix already wired into run-next.mjs + // (the dev-only entry point) — otherwise real installs never got it. Raise + // both timeouts well above any realistic client idle-pool window, mirroring + // src/lib/apiBridgeServer.ts's pattern. + const mainServerTimeouts = getMainServerTimeoutConfig(); + server.keepAliveTimeout = mainServerTimeouts.keepAliveTimeoutMs; + server.headersTimeout = mainServerTimeouts.headersTimeoutMs; const originalOn = server.on.bind(server); const originalAddListener = server.addListener.bind(server); diff --git a/scripts/homolog/gen-promptfoo.mjs b/scripts/homolog/gen-promptfoo.mjs new file mode 100644 index 0000000000..789f147076 --- /dev/null +++ b/scripts/homolog/gen-promptfoo.mjs @@ -0,0 +1,52 @@ +import fs from "node:fs"; +import path from "node:path"; +import { pickSmokeModels } from "./lib/providerTiers.mjs"; + +const baseUrl = process.env.HOMOLOG_BASE_URL; +const critical = (process.env.HOMOLOG_CRITICAL_PROVIDERS || "").split(",").filter(Boolean); + +const res = await fetch(`${baseUrl}/v1/models`, { + headers: { Authorization: `Bearer ${process.env.HOMOLOG_API_KEY}` }, +}); +if (!res.ok) throw new Error(`/v1/models HTTP ${res.status}`); +const catalog = (await res.json()).data; + +const picks = pickSmokeModels(catalog, critical); +const missing = picks.filter((p) => !p.model); +const providers = picks + .filter((p) => p.model) + .map((p) => ({ + id: `openai:chat:${p.model}`, + label: p.provider, + config: { + apiBaseUrl: `${baseUrl}/v1`, + apiKeyEnvar: "HOMOLOG_API_KEY", + max_tokens: 5, + temperature: 0, + // OmniRoute streama por default quando "stream" é omitido (streamDefaultMode + // legacy) — o parser JSON do promptfoo precisa da resposta non-stream. + passthrough: { stream: false, max_tokens: 5 }, + }, + })); + +const config = { + description: "OmniRoute homolog — smoke real 1 request/provider crítico", + prompts: ["Reply with exactly: OK"], + providers, + // O smoke valida o WIRING do provider (respondeu sem erro), não o comportamento + // do modelo: com max_tokens=5, modelos de reasoning podem gastar o budget antes + // de emitir o "OK" literal — icontains seria falso-positivo de quebra. + tests: [{ assert: [{ type: "javascript", value: "typeof output === 'string'" }] }], +}; +fs.mkdirSync("homolog-report/raw", { recursive: true }); +fs.writeFileSync( + path.join("homolog-report", "promptfooconfig.yaml"), + JSON.stringify(config, null, 2) // promptfoo aceita JSON como config YAML-compatível +); +fs.writeFileSync( + path.join("homolog-report", "raw", "provider-misses.json"), + JSON.stringify(missing, null, 2) +); +console.log( + `[gen-promptfoo] ${providers.length} providers no smoke, ${missing.length} misses de catálogo` +); diff --git a/scripts/homolog/lib/adminClient.mjs b/scripts/homolog/lib/adminClient.mjs new file mode 100644 index 0000000000..5f8c78e7cc --- /dev/null +++ b/scripts/homolog/lib/adminClient.mjs @@ -0,0 +1,65 @@ +// Cookie confirmado em src/app/api/auth/login/route.ts (cookieStore.set("auth_token", ...)) +// e em src/shared/utils/apiAuth.ts (isDashboardSessionAuthenticated lê "auth_token"). +const TOKEN_COOKIE = "auth_token"; + +export function extractJwtCookie(setCookies) { + for (const c of setCookies || []) { + const m = c.match(new RegExp(`^(${TOKEN_COOKIE}=[^;]+)`)); + if (m) return m[1]; + } + return null; +} + +export function extractApiKey(body) { + if (!body?.key || !body?.id) throw new Error("POST /api/keys sem key/id no corpo"); + return { key: body.key, id: body.id }; +} + +// fetch com 1 retry para erros de socket (keep-alive reciclado pelo servidor +// entre requests espaçados derruba o 1º write com EPIPE/other side closed). +async function fetchRetry(url, init, retries = 1) { + try { + return await fetch(url, init); + } catch (err) { + if (retries > 0) { + await new Promise((r) => setTimeout(r, 1_000)); + return fetchRetry(url, init, retries - 1); + } + throw err; + } +} + +/** Login admin → cria API key efêmera. Retorna {key, id, cookie, revoke()}. */ +export async function createEphemeralKey(baseUrl, password) { + const login = await fetchRetry(`${baseUrl}/api/auth/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ password }), + }); + if (!login.ok) throw new Error(`login falhou: HTTP ${login.status}`); + const cookie = extractJwtCookie(login.headers.getSetCookie()); + if (!cookie) throw new Error("login sem cookie de sessão"); + + // sufixo único por run: dois runs paralelos (ou um cleanup por nome) nunca colidem + const name = `homolog-${new Date().toISOString().slice(0, 10)}-${Math.random().toString(36).slice(2, 8)}`; + const create = await fetchRetry(`${baseUrl}/api/keys`, { + method: "POST", + headers: { "Content-Type": "application/json", cookie }, + body: JSON.stringify({ name }), + }); + if (!create.ok) throw new Error(`criação de key falhou: HTTP ${create.status}`); + const { key, id } = extractApiKey(await create.json()); + + return { + key, + id, + cookie, + async revoke() { + const del = await fetchRetry(`${baseUrl}/api/keys/${id}`, { + method: "DELETE", + headers: { cookie }, + }); + if (!del.ok) throw new Error(`revogação da key ${id} falhou: HTTP ${del.status}`); + }, + }; +} diff --git a/scripts/homolog/lib/parity.mjs b/scripts/homolog/lib/parity.mjs new file mode 100644 index 0000000000..0fa00ea37c --- /dev/null +++ b/scripts/homolog/lib/parity.mjs @@ -0,0 +1,15 @@ +/** + * Avaliação pura de paridade do deploy (testável sem rede). + * @param {{status?: string, version?: string}} health corpo de /api/monitoring/health + * @param {{expectedVersion: string, httpStatus: number}} ctx + * @returns {{ok: boolean, failures: string[]}} + */ +export function evaluateParity(health, ctx) { + const failures = []; + if (ctx.httpStatus !== 200) failures.push(`health HTTP ${ctx.httpStatus} (esperado 200)`); + if (health?.status !== "healthy") + failures.push(`status "${health?.status}" (esperado "healthy")`); + if (health?.version !== ctx.expectedVersion) + failures.push(`version "${health?.version}" (esperado "${ctx.expectedVersion}")`); + return { ok: failures.length === 0, failures }; +} diff --git a/scripts/homolog/lib/promptfooToCtrf.mjs b/scripts/homolog/lib/promptfooToCtrf.mjs new file mode 100644 index 0000000000..06712f29ae --- /dev/null +++ b/scripts/homolog/lib/promptfooToCtrf.mjs @@ -0,0 +1,24 @@ +export function promptfooToCtrf(output) { + const rows = output?.results?.results || []; + const tests = rows.map((r) => ({ + name: `provider-smoke: ${r.provider?.label || r.provider?.id || "?"}`, + status: r.success ? "passed" : "failed", + duration: Math.round(r.latencyMs || 0), + ...(r.error ? { message: String(r.error).slice(0, 300) } : {}), + })); + const passed = tests.filter((t) => t.status === "passed").length; + return { + results: { + tool: { name: "promptfoo" }, + summary: { + tests: tests.length, + passed, + failed: tests.length - passed, + pending: 0, + skipped: 0, + other: 0, + }, + tests, + }, + }; +} diff --git a/scripts/homolog/lib/providerTiers.mjs b/scripts/homolog/lib/providerTiers.mjs new file mode 100644 index 0000000000..c10a8a0300 --- /dev/null +++ b/scripts/homolog/lib/providerTiers.mjs @@ -0,0 +1,7 @@ +/** Escolhe 1 modelo por provider crítico a partir do catálogo /v1/models. */ +export function pickSmokeModels(catalog, criticalProviders) { + return criticalProviders.map((provider) => { + const hit = catalog.find((m) => m.id.startsWith(`${provider}/`)); + return { provider, model: hit ? hit.id : null }; + }); +} diff --git a/scripts/homolog/lib/sseCheck.mjs b/scripts/homolog/lib/sseCheck.mjs new file mode 100644 index 0000000000..bc5ec2f90f --- /dev/null +++ b/scripts/homolog/lib/sseCheck.mjs @@ -0,0 +1,80 @@ +export function parseSseChunk(text) { + // Itera LINHAS dentro de cada bloco: a VPS emite comment-lines SSE + // (": x-omniroute-*") no mesmo bloco do "data: [DONE]", então olhar só o + // início do bloco perde o terminador. + const events = []; + for (const block of text.split(/\n\n/)) { + for (const line of block.split("\n")) { + const t = line.trim(); + if (t.startsWith("data:")) events.push(t.slice(5).trim()); + } + } + return events; +} + +export function summarizeStream(events) { + let contentDeltas = 0; + let done = false; + for (const e of events) { + if (e === "[DONE]") { + done = true; + continue; + } + try { + const j = JSON.parse(e); + if (j.choices?.[0]?.delta?.content) contentDeltas++; + } catch { + /* fragmento parcial — ignorado; o caller acumula buffer */ + } + } + const ok = contentDeltas >= 1 && done; + return { ok, contentDeltas, done }; +} + +/** Faz 1 chat streaming real e valida o protocolo SSE ponta-a-ponta. */ +export async function checkSse(baseUrl, apiKey, model, { retries = 1 } = {}) { + try { + const res = await fetch(`${baseUrl}/v1/chat/completions`, { + method: "POST", + headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" }, + body: JSON.stringify({ + model, + messages: [{ role: "user", content: "Reply with exactly: OK" }], + max_tokens: 5, + stream: true, + }), + }); + if (res.status !== 200) return { ok: false, failures: [`HTTP ${res.status}`] }; + const ct = res.headers.get("content-type") || ""; + if (!ct.includes("text/event-stream")) return { ok: false, failures: [`content-type "${ct}"`] }; + + const events = []; + let buffer = ""; + const reader = res.body.getReader(); + const decoder = new TextDecoder(); + const deadline = Date.now() + 60_000; + while (Date.now() < deadline) { + const { done, value } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + const lastSep = buffer.lastIndexOf("\n\n"); + if (lastSep >= 0) { + events.push(...parseSseChunk(buffer.slice(0, lastSep + 2))); + buffer = buffer.slice(lastSep + 2); + } + } + // flush do resto do buffer (último bloco pode chegar sem "\n\n" no read final) + if (buffer.trim()) events.push(...parseSseChunk(buffer)); + const s = summarizeStream(events); + return { ok: s.ok, failures: s.ok ? [] : [`contentDeltas=${s.contentDeltas} done=${s.done}`] }; + } catch (err) { + // Socket keep-alive reciclado pelo servidor entre requests é transitório — + // 1 retry antes de reportar falha. Erro persistente é FALHA da camada, + // nunca crash do orquestrador. + if (retries > 0) { + await new Promise((r) => setTimeout(r, 1_000)); + return checkSse(baseUrl, apiKey, model, { retries: retries - 1 }); + } + return { ok: false, failures: [`fetch/stream error: ${err?.cause?.message || err.message}`] }; + } +} diff --git a/scripts/homolog/run.mjs b/scripts/homolog/run.mjs new file mode 100644 index 0000000000..f1b01be437 --- /dev/null +++ b/scripts/homolog/run.mjs @@ -0,0 +1,169 @@ +#!/usr/bin/env node +import { execSync, spawnSync } from "node:child_process"; +import fs from "node:fs"; +import { evaluateParity } from "./lib/parity.mjs"; +import { createEphemeralKey } from "./lib/adminClient.mjs"; +import { checkSse } from "./lib/sseCheck.mjs"; +import { promptfooToCtrf } from "./lib/promptfooToCtrf.mjs"; + +// ── env ────────────────────────────────────────────────────────────────── +if (fs.existsSync(".env.homolog")) { + for (const line of fs.readFileSync(".env.homolog", "utf8").split("\n")) { + const m = line.match(/^([A-Z_]+)=(.*)$/); + if (m && !process.env[m[1]]) process.env[m[1]] = m[2]; + } +} +const BASE = process.env.HOMOLOG_BASE_URL; +if (!BASE || !process.env.HOMOLOG_ADMIN_PASSWORD) { + console.error("Configure .env.homolog (HOMOLOG_BASE_URL, HOMOLOG_ADMIN_PASSWORD)"); + process.exit(2); +} +fs.rmSync("homolog-report", { recursive: true, force: true }); +// raw/ fica FORA do merge CTRF: `ctrf merge` tenta mesclar qualquer *.json com +// chave "results" e quebra no output cru do promptfoo. +fs.mkdirSync("homolog-report/raw", { recursive: true }); +const layers = []; // {name, ok, detail} +const record = (name, ok, detail = "") => { + layers.push({ name, ok, detail }); + console.log(`${ok ? "✅" : "❌"} ${name}${detail ? ` — ${detail}` : ""}`); +}; + +// ── L0 saúde/paridade ──────────────────────────────────────────────────── +const expectedVersion = + process.env.HOMOLOG_EXPECT_VERSION || JSON.parse(fs.readFileSync("package.json", "utf8")).version; +const healthRes = await fetch(`${BASE}/api/monitoring/health`); +const health = await healthRes.json().catch(() => ({})); +const parity = evaluateParity(health, { expectedVersion, httpStatus: healthRes.status }); +record("L0 saúde/paridade", parity.ok, parity.failures.join("; ")); +if (!parity.ok) { + console.error("Deploy divergente — abortando."); + writeSummary(layers, BASE, expectedVersion); + process.exit(1); +} + +// ── chave efêmera ──────────────────────────────────────────────────────── +const eph = await createEphemeralKey(BASE, process.env.HOMOLOG_ADMIN_PASSWORD); +process.env.HOMOLOG_API_KEY = eph.key; +try { + // modelo de smoke = 1º do tier crítico presente no catálogo + const models = ( + await ( + await fetch(`${BASE}/v1/models`, { headers: { Authorization: `Bearer ${eph.key}` } }) + ).json() + ).data; + const critical = (process.env.HOMOLOG_CRITICAL_PROVIDERS || "openai").split(","); + const smokeModel = + models.find((m) => critical.some((p) => m.id.startsWith(`${p}/`)))?.id || models[0].id; + + // ── L1 httpYac + SSE ─────────────────────────────────────────────────── + const hy = spawnSync( + "npx", + [ + "httpyac", + "send", + "tests/homolog/api/core.http", + "--all", + "--var", + `baseUrl=${BASE}`, + "--var", + `apiKey=${eph.key}`, + "--var", + `smokeModel=${smokeModel}`, + "--junit", + "--output", + "none", + ], + { encoding: "utf8" } + ); + fs.writeFileSync("homolog-report/httpyac-junit.xml", hy.stdout || ""); + record("L1 API (httpYac)", hy.status === 0); + const sse = await checkSse(BASE, eph.key, smokeModel); + record("L1 SSE streaming", sse.ok, (sse.failures || []).join("; ")); + + // ── L2 providers reais ───────────────────────────────────────────────── + try { + execSync("node scripts/homolog/gen-promptfoo.mjs", { stdio: "inherit", env: process.env }); + spawnSync( + "npx", + [ + "promptfoo", + "eval", + "-c", + "homolog-report/promptfooconfig.yaml", + "-o", + "homolog-report/raw/promptfoo.json", + "--no-cache", + ], + { encoding: "utf8", env: process.env } + ); + const pfOut = JSON.parse(fs.readFileSync("homolog-report/raw/promptfoo.json", "utf8")); + const pfCtrf = promptfooToCtrf(pfOut); + fs.writeFileSync("homolog-report/providers-ctrf.json", JSON.stringify(pfCtrf, null, 2)); + record( + "L2 providers reais", + pfCtrf.results.summary.failed === 0, + `${pfCtrf.results.summary.passed}/${pfCtrf.results.summary.tests} providers OK` + ); + } catch (err) { + // gerador/eval quebrando é falha da camada — o run continua para o L4 e o cleanup + record("L2 providers reais", false, err.message); + } + + // ── L4 UI ────────────────────────────────────────────────────────────── + const pw = spawnSync( + "npx", + ["playwright", "test", "-c", "tests/homolog/ui/playwright.config.ts"], + { + stdio: "inherit", + env: process.env, + } + ); + record("L4 UI (Playwright)", pw.status === 0); +} finally { + await eph + .revoke() + .then(() => record("cleanup: key efêmera revogada", true)) + .catch((e) => record("cleanup: key efêmera revogada", false, e.message)); +} + +// ── L5 relatório unificado ─────────────────────────────────────────────── +spawnSync( + "npx", + ["junit-to-ctrf", "homolog-report/httpyac-junit.xml", "-o", "homolog-report/api-ctrf.json"], + { + stdio: "inherit", + } +); +spawnSync( + "npx", + [ + "ctrf", + "merge", + "homolog-report", + "--output", + "homolog-ctrf.json", + "--output-dir", + "homolog-report", + ], + { + stdio: "inherit", + } +); + +writeSummary(layers, BASE, expectedVersion); +const failed = layers.filter((l) => !l.ok); +process.exit(failed.length ? 1 : 0); + +function writeSummary(rows, base, version) { + const md = [ + "# Homologação — relatório", + "", + `Alvo: ${base} · versão esperada: ${version}`, + "", + "| camada | resultado | detalhe |", + "|---|---|---|", + ...rows.map((l) => `| ${l.name} | ${l.ok ? "✅" : "❌"} | ${l.detail} |`), + ].join("\n"); + fs.writeFileSync("homolog-report/summary.md", md); + console.log(`\n${md}\n\nRelatório: homolog-report/ (CTRF unificado: homolog-ctrf.json)`); +} diff --git a/scripts/ops/runner-janitor.sh b/scripts/ops/runner-janitor.sh new file mode 100755 index 0000000000..99c081b10f --- /dev/null +++ b/scripts/ops/runner-janitor.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# runner-janitor — self-hosted runner box hygiene (WS3.3, v3.8.49 quality plan). +# +# The .113 runner box has recurring failure modes that until now were manual +# discipline: orphaned tmpfs/work dirs filling the disk, and >4 concurrent +# runners OOM-killing jobs (16 GB box; incidents on the v3.8.47 release day). +# Install via cron on the box (see docs/ops/RUNNER_BOX.md): +# */30 * * * * /opt/omniroute-ops/runner-janitor.sh >> /var/log/runner-janitor.log 2>&1 +# +# Exit codes: 0 healthy · 1 attention needed (printed to stdout for the log). +set -euo pipefail + +MAX_ACTIVE_RUNNERS="${MAX_ACTIVE_RUNNERS:-4}" +DISK_ALERT_PCT="${DISK_ALERT_PCT:-85}" +WORK_DIR_MAX_AGE_HOURS="${WORK_DIR_MAX_AGE_HOURS:-24}" +STATUS=0 + +echo "[janitor] $(date -u +%FT%TZ) start" + +# 1) Sweep stale runner temp/work leftovers (>24h — no legitimate job runs that long). +# Hardened for a root cron on world-writable paths: never follow a symlinked base +# (a compromised runner could plant one), -P + -xdev so the sweep cannot traverse +# out of the filesystem, and patterns narrowed to names OUR tooling creates +# (no generic tmp* — unrelated system temp files are out of scope). +for base in /tmp /home/*/actions-runner*/_work/_temp; do + [ -d "$base" ] || continue + [ -L "$base" ] && { echo "[janitor] skip symlinked base: $base"; continue; } + find -P "$base" -xdev -maxdepth 1 \( -name 'runner-*' -o -name 'omniroute-*' \) \ + ! -type l -mmin +$((WORK_DIR_MAX_AGE_HOURS * 60)) -exec rm -rf {} + 2>/dev/null || true +done +echo "[janitor] stale temp sweep done" + +# 2) Disk pressure — alert loudly before SQLITE_FULL kills jobs mid-run. +USAGE=$(df --output=pcent / | tail -1 | tr -dc '0-9') +if [ "$USAGE" -ge "$DISK_ALERT_PCT" ]; then + echo "[janitor] ⚠ ROOT DISK ${USAGE}% >= ${DISK_ALERT_PCT}% — clean before the next heavy run" + STATUS=1 +else + echo "[janitor] disk ${USAGE}% OK" +fi + +# 3) Concurrency ceiling — 8-wide OOMed the 16 GB box twice on release day; +# 4 is the proven ceiling. This CODIFIES the rule that was manual discipline. +ACTIVE=$(pgrep -fc "Runner.Listener" || true) +if [ "${ACTIVE:-0}" -gt "$MAX_ACTIVE_RUNNERS" ]; then + echo "[janitor] ⚠ ${ACTIVE} Runner.Listener processes > ceiling ${MAX_ACTIVE_RUNNERS} — stop the extra runners (systemctl stop actions.runner.)" + STATUS=1 +else + echo "[janitor] runners active: ${ACTIVE:-0}/${MAX_ACTIVE_RUNNERS} OK" +fi + +echo "[janitor] done status=$STATUS" +exit "$STATUS" diff --git a/scripts/quality/balance-e2e-shards.mjs b/scripts/quality/balance-e2e-shards.mjs new file mode 100644 index 0000000000..450a5438bb --- /dev/null +++ b/scripts/quality/balance-e2e-shards.mjs @@ -0,0 +1,99 @@ +#!/usr/bin/env node +/** + * balance-e2e-shards — duration-aware LPT bin-packing for the Playwright matrix (WS4.1). + * + * Playwright's --shard=N/M distributes by COUNT (per file with fullyParallel:false), + * blind to duration — measured skew on the 9-shard matrix: worst 24m47s vs best 1m47s + * (14×), putting E2E on the CI critical path. This script assigns spec FILES to shards + * by weight (Longest Processing Time greedy: heaviest first, always into the lightest + * shard) using config/quality/e2e-timings.json, and prints shard N's files (one per + * line) for `npx playwright test $FILES`. + * + * Safety: the union of all shards is verified to equal the discovered spec list — + * losing a spec silently would hollow the suite. Any inconsistency (or a missing + * timings file) exits non-zero so the CI step falls back to plain --shard=N/M. + * + * Weights are RELATIVE (unitless). The seed uses LOC as a proxy; regenerate from real + * durations by editing config/quality/e2e-timings.json (see its _meta note). + */ +import fs from "node:fs"; +import path from "node:path"; + +const E2E_DIR = path.join("tests", "e2e"); +const TIMINGS_PATH = path.join("config", "quality", "e2e-timings.json"); + +/** + * LPT greedy assignment. Deterministic: weight desc, then filename asc; ties on + * shard totals resolve to the lowest shard index. + * @param {{file: string, weight: number}[]} items + * @param {number} shardCount + * @returns {{files: string[], total: number}[]} + */ +export function lptAssign(items, shardCount) { + const shards = Array.from({ length: shardCount }, () => ({ files: [], total: 0 })); + const sorted = [...items].sort( + (a, b) => b.weight - a.weight || a.file.localeCompare(b.file) + ); + for (const item of sorted) { + let target = shards[0]; + for (const s of shards) if (s.total < target.total) target = s; + target.files.push(item.file); + target.total += item.weight; + } + return shards; +} + +/** + * Weight lookup with a median fallback so a NEW spec (no timing yet) lands mid-pack + * instead of skewing a shard. + * @param {string[]} files basenames + * @param {Record} timings + */ +export function weightItems(files, timings) { + const known = Object.entries(timings) + .filter(([k]) => !k.startsWith("_")) + .map(([, v]) => v) + .filter((v) => Number.isFinite(v) && v > 0) + .sort((a, b) => a - b); + const median = known.length ? known[Math.floor(known.length / 2)] : 1; + return files.map((file) => ({ + file, + weight: Number.isFinite(timings[file]) && timings[file] > 0 ? timings[file] : median, + })); +} + +function main() { + const shard = Number(process.argv[2]); + const total = Number(process.argv[3]); + if (!Number.isInteger(shard) || !Number.isInteger(total) || shard < 1 || shard > total) { + console.error("usage: node scripts/quality/balance-e2e-shards.mjs "); + process.exit(2); + } + if (!fs.existsSync(TIMINGS_PATH)) { + console.error(`[e2e-balance] ${TIMINGS_PATH} missing — caller should fall back to --shard`); + process.exit(3); + } + const timings = JSON.parse(fs.readFileSync(TIMINGS_PATH, "utf8")); + const files = fs + .readdirSync(E2E_DIR) + .filter((f) => f.endsWith(".spec.ts")) + .sort(); + if (!files.length) { + console.error(`[e2e-balance] no specs found under ${E2E_DIR}`); + process.exit(3); + } + const shards = lptAssign(weightItems(files, timings), total); + const assigned = shards.flatMap((s) => s.files).sort(); + if (assigned.length !== files.length || assigned.some((f, i) => f !== files[i])) { + console.error("[e2e-balance] INTERNAL: shard union != discovered specs — falling back"); + process.exit(3); + } + process.stdout.write( + shards[shard - 1].files.map((f) => path.join(E2E_DIR, f)).join("\n") + "\n" + ); +} + +const isDirectRun = + process.argv[1] && + path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname); +if (isDirectRun) main(); diff --git a/scripts/quality/classify-pr-changes.mjs b/scripts/quality/classify-pr-changes.mjs index d1cff98f0f..b1ad58fd2a 100644 --- a/scripts/quality/classify-pr-changes.mjs +++ b/scripts/quality/classify-pr-changes.mjs @@ -17,19 +17,28 @@ import { fileURLToPath } from "node:url"; /** * @param {string[]} files relative paths from git diff - * @returns {{ code: boolean, docs: boolean, i18n: boolean, workflow: boolean }} + * @returns {{ code: boolean, docs: boolean, i18n: boolean, workflow: boolean, testsOnly: boolean }} */ export function classifyPaths(files) { let code = false; let docs = false; let i18n = false; let workflow = false; + // testsOnly (WS3.1 fast lane): every file lives under tests/ AND none is an e2e + // spec — such a diff cannot change the served app, so the E2E matrix may skip. + // Changing tests/e2e/** REQUIRES running e2e, so it is excluded from the shortcut. + let sawAnyFile = false; + let sawNonTest = false; + let sawE2eTest = false; for (const raw of files) { const f = String(raw || "") .trim() .replace(/\\/g, "/"); if (!f) continue; + sawAnyFile = true; + if (f.startsWith("tests/e2e/")) sawE2eTest = true; + else if (!f.startsWith("tests/")) sawNonTest = true; if (f.startsWith(".github/workflows/") || f === ".zizmor.yml") { workflow = true; @@ -84,7 +93,7 @@ export function classifyPaths(files) { code = true; } - return { code, docs, i18n, workflow }; + return { code, docs, i18n, workflow, testsOnly: sawAnyFile && !sawNonTest && !sawE2eTest }; } function main() { @@ -115,7 +124,7 @@ function main() { const c = classifyPaths(files); // GitHub Actions output format (also human-readable key=value). process.stdout.write( - `code=${c.code}\ndocs=${c.docs}\ni18n=${c.i18n}\nworkflow=${c.workflow}\n` + `code=${c.code}\ndocs=${c.docs}\ni18n=${c.i18n}\nworkflow=${c.workflow}\ntestsOnly=${c.testsOnly}\n` ); } diff --git a/scripts/quality/validate-release-green.mjs b/scripts/quality/validate-release-green.mjs index db0933ed67..a14b80051d 100644 --- a/scripts/quality/validate-release-green.mjs +++ b/scripts/quality/validate-release-green.mjs @@ -126,7 +126,14 @@ export function parseEslintJson(out) { /** Pull the cognitive-complexity violation count from the gate's output. */ export function parseCognitiveCount(out) { - const m = String(out || "").match(/(\d+)\s+(?:function\(s\) exceed|violações|violations)/i); + const s = String(out || ""); + // `check:complexity-ratchets` runs ONE shared ESLint walk and prints BOTH ratchets, with the + // cyclomatic "N violações" summary emitted FIRST — so a bare `\d+ violações` regex would grab + // the cyclomatic count. Prefer the unambiguous machine-readable `cognitiveComplexity=N` line + // (mirrors the cyclomatic `complexity=N` parse used for cycCurrent below). + const machine = s.match(/(?:^|\n)cognitiveComplexity=(\d+)/); + if (machine) return Number(machine[1]); + const m = s.match(/(\d+)\s+(?:function\(s\) exceed|violações|violations)/i); return m ? Number(m[1]) : null; } @@ -543,6 +550,14 @@ async function main() { args: ["run", "check:pack-artifact"], timeout: 20 * 60 * 1000, }); + // WS1.2 (#7065 class): boot the REAL packed tarball from a clean install — + // the runtime gate structure checks cannot provide. Reuses the same dist/ build. + slow.push({ + id: "pack-boot", + label: "Tarball boot-smoke (installed CLI serves /health)", + args: ["run", "check:pack-boot"], + timeout: 15 * 60 * 1000, + }); } slow.forEach((g) => announce(`${g.label} [parallel]`)); const slowResults = await Promise.all( diff --git a/scripts/release/rehome-open-prs.mjs b/scripts/release/rehome-open-prs.mjs new file mode 100644 index 0000000000..934a0987b2 --- /dev/null +++ b/scripts/release/rehome-open-prs.mjs @@ -0,0 +1,158 @@ +#!/usr/bin/env node +// scripts/release/rehome-open-prs.mjs +// +// Parallel-cycle PR re-home (generate-release Phase 0a.0b, step 3). +// Retargets every open PR whose base is the FROZEN release/v onto the +// freshly cut release/v, so development keeps flowing while the captain +// owns the frozen branch. Design: _tasks/release-flow/2026-07-04_proposta-ciclo-paralelo-v2.md +// +// Usage: +// node scripts/release/rehome-open-prs.mjs [--dry-run] +// e.g. node scripts/release/rehome-open-prs.mjs 3.8.49 3.8.50 +// +// WHY THIS EXISTS AS A SCRIPT AND NOT A `gh pr edit` LOOP IN THE SKILL: +// +// 1. `gh pr edit --base` FAILS SILENTLY (v3.8.42 lesson). It exits 0 while +// leaving the base untouched — so every edit MUST be read back with +// `gh pr view --json baseRefName`. A hand-run loop skips that under +// fatigue; this does not. +// 2. Volume. At the v3.8.49 freeze there were 148 open PRs on the release +// branch — ~450 API calls between edit, verify and comment. That is not a +// thing a human does reliably at 2am mid-release. +// 3. `gh pr list` defaults to **30 results**. A loop written without +// `--limit` silently re-homes the first 30 and reports success. +// +// Idempotent: a PR already based on release/v is skipped, so a resumed +// release re-runs this safely. +// +// NOT covered here (by design): PRs opened AFTER this runs. Those are handled +// by flipping the repo's default_branch to release/v at 0a.0b — see the +// skill. Contributors open PRs against the default branch; if that still points +// at `main`, they never target a release branch at all. + +import { execFileSync } from "node:child_process"; +import { pathToFileURL } from "node:url"; + +const REPO = "diegosouzapw/OmniRoute"; + +function gh(args, { allowFail = false } = {}) { + try { + return execFileSync("gh", args, { encoding: "utf8" }).trim(); + } catch (err) { + if (allowFail) return null; + throw new Error(`gh ${args.join(" ")} failed: ${err.stderr || err.message}`); + } +} + +/** + * Pure: classify what should happen to a PR given its current base. + * Split out so the decision is unit-testable without touching the network. + */ +export function classify(pr, currentBase, nextBase) { + if (pr.baseRefName === nextBase) return { action: "skip", reason: "already re-homed" }; + if (pr.baseRefName !== currentBase) { + return { action: "skip", reason: `base is ${pr.baseRefName}, not the frozen branch` }; + } + if (pr.isDraft) return { action: "retarget", reason: "draft — retarget anyway, it still needs a home" }; + return { action: "retarget", reason: "open PR on the frozen branch" }; +} + +function main(argv) { + const dryRun = argv.includes("--dry-run"); + const [current, next] = argv.filter((a) => !a.startsWith("--")); + + if (!current || !next) { + console.error("Usage: node scripts/release/rehome-open-prs.mjs [--dry-run]"); + console.error(" e.g. node scripts/release/rehome-open-prs.mjs 3.8.49 3.8.50"); + process.exit(2); + } + + const currentBase = `release/v${current}`; + const nextBase = `release/v${next}`; + + // The next branch MUST exist before we point anything at it, or every edit + // 422s and we have re-homed nothing while reporting progress. + const exists = gh(["api", `repos/${REPO}/branches/${nextBase}`, "--jq", ".name"], { + allowFail: true, + }); + if (!exists) { + console.error(`✖ ${nextBase} does not exist on origin — cut it first (0a.0b step 1).`); + process.exit(1); + } + + // --limit 300: `gh pr list` returns 30 by default. Without this the loop + // silently re-homes a third of the queue and exits 0. + const raw = gh([ + "pr", "list", "--repo", REPO, "--state", "open", "--limit", "300", + "--base", currentBase, "--json", "number,title,isDraft,baseRefName", + ]); + const prs = JSON.parse(raw); + + console.log(`${prs.length} open PR(s) on ${currentBase} → ${nextBase}${dryRun ? " [DRY RUN]" : ""}\n`); + + const failed = []; + let moved = 0; + let skipped = 0; + + for (const pr of prs) { + const { action, reason } = classify(pr, currentBase, nextBase); + if (action === "skip") { + console.log(` · #${pr.number} skipped — ${reason}`); + skipped++; + continue; + } + if (dryRun) { + console.log(` → #${pr.number} would retarget — ${reason}`); + moved++; + continue; + } + + gh(["pr", "edit", String(pr.number), "--repo", REPO, "--base", nextBase], { allowFail: true }); + + // The read-back is the whole point: `gh pr edit --base` exits 0 on failure. + const actual = gh( + ["pr", "view", String(pr.number), "--repo", REPO, "--json", "baseRefName", "--jq", ".baseRefName"], + { allowFail: true } + ); + + if (actual !== nextBase) { + console.error(` ✖ #${pr.number} STILL on ${actual ?? "?"} — retarget did not take`); + failed.push({ number: pr.number, actual }); + continue; + } + + gh([ + "pr", "comment", String(pr.number), "--repo", REPO, + "--body", + `Re-homed to \`${nextBase}\`: v${current} entered its release freeze, so the branch now belongs ` + + `to the release captain and development continues on the next cycle. Nothing is wrong with this ` + + `PR — it just needed a live base. No action needed from you; CI will re-run against the new base.`, + ], { allowFail: true }); + + console.log(` ✔ #${pr.number} → ${nextBase}`); + moved++; + } + + console.log(`\n${moved} re-homed, ${skipped} skipped, ${failed.length} failed`); + + if (failed.length) { + console.error( + `\n✖ ${failed.length} PR(s) did not take the retarget: ${failed.map((f) => `#${f.number}`).join(", ")}\n` + + ` Re-run this script (it is idempotent) or retarget those by hand and verify with\n` + + ` gh pr view --json baseRefName` + ); + process.exit(1); + } + + if (!dryRun && moved > 0) { + console.log( + `\nReminder (0a.0b): flip the repo default_branch so PRs opened from now on are born on the\n` + + `right base — this script cannot reach PRs that do not exist yet:\n` + + ` gh api -X PATCH repos/${REPO} -f default_branch="${nextBase}"` + ); + } +} + +if (import.meta.url === pathToFileURL(process.argv[1]).href) { + main(process.argv.slice(2)); +} diff --git a/scripts/release/sync-next-cycle.mjs b/scripts/release/sync-next-cycle.mjs index 3a223baad3..ea2463cd99 100644 --- a/scripts/release/sync-next-cycle.mjs +++ b/scripts/release/sync-next-cycle.mjs @@ -106,6 +106,15 @@ function git(args, opts = {}) { return execFileSync("git", args, { encoding: "utf8", maxBuffer: 64 * 1024 * 1024, ...opts }).trim(); } +// The sync-back is the ONE write path to the release branch with no CI gate — a red +// merged tree pushed here turns the whole PR queue red (G1, v3.8.49 quality plan WS0.3). +// Returns the validate-release-green invocation to run before the push, or null when +// the operator passed --skip-green-gate (emergency hatch: tip reds verified pre-existing). +export function greenGateArgs(argv) { + if (argv.includes("--skip-green-gate")) return null; + return ["scripts/quality/validate-release-green.mjs", "--quick"]; +} + function main() { const NEXT = process.argv[2]; if (!/^\d+\.\d+\.\d+$/.test(NEXT || "")) { @@ -209,6 +218,29 @@ function main() { } git(["commit", "-m", `chore(release): sync main (v${prevVersion} close) into ${BRANCH} — parallel-cycle sync-back`], { cwd: WT }); + + // WS0.3 green gate: validate the MERGED tree before it reaches origin. The commit + // stays local on failure so the captain can inspect/fix in the sync worktree. + const gate = greenGateArgs(process.argv); + if (gate) { + const nm = path.join(WT, "node_modules"); + if (!fs.existsSync(nm)) fs.symlinkSync(path.join(ROOT, "node_modules"), nm, "dir"); + console.log("[sync-next-cycle] release-green --quick on the merged tree (pre-push gate)…"); + try { + execFileSync("node", gate, { cwd: WT, stdio: "inherit", maxBuffer: 64 * 1024 * 1024 }); + } catch { + console.error( + `[sync-next-cycle] ABORT: release-green --quick found HARD failures in the merged tree.` + + `\n The sync commit is LOCAL-ONLY in ${WT} — fix the reds there, then re-run this script.` + + `\n Use --skip-green-gate ONLY after verifying the reds pre-exist on origin/${BRANCH}.` + ); + process.exit(1); + } + fs.rmSync(nm, { force: true }); + } else { + console.warn("[sync-next-cycle] ⚠ --skip-green-gate: pushing WITHOUT release-green validation."); + } + git(["push", "origin", BRANCH], { cwd: WT }); const left = git(["rev-list", "--count", `${BRANCH}..origin/main`], { cwd: WT }); diff --git a/scripts/release/verify-published.mjs b/scripts/release/verify-published.mjs new file mode 100644 index 0000000000..cdab080daf --- /dev/null +++ b/scripts/release/verify-published.mjs @@ -0,0 +1,106 @@ +#!/usr/bin/env node +/** + * verify-published — post-publish net for the npm channel (WS1.4, #7065 class). + * + * After `npm stage approve` (or any publish), install the PUBLISHED version from the + * PUBLIC registry inside a clean `node:24-slim` container and boot it to a healthy + * /api/monitoring/health that reports the expected version. This is the last net: + * it validates the exact bytes users will install, on a machine with none of our + * repo/devbox state. Wired into /generate-release Phase 4 (monitoring). + * + * Usage: node scripts/release/verify-published.mjs + * Requires Docker (the clean container IS the point). Exit: 0 verified · + * 1 boot/version failure · 2 bad usage / docker unavailable. + */ +import { execFileSync, spawnSync } from "node:child_process"; + +const BOOT_DEADLINE_S = 240; +const PORT = 23987; + +/** Strict semver (with optional prerelease) — the version reaches a shell inside + * the container via env, but validate anyway (Hard Rule #13 defense in depth). */ +export function parseVersionArg(arg) { + if (!/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(arg || "")) return null; + return arg; +} + +/** docker invocation — version and knobs travel as env vars, never interpolated + * into the script body (Hard Rule #13). */ +export function buildDockerArgs(version) { + return [ + "run", + "--rm", + "-e", + `VERIFY_VERSION=${version}`, + "-e", + `VERIFY_PORT=${PORT}`, + "-e", + `VERIFY_DEADLINE_S=${BOOT_DEADLINE_S}`, + "node:24-slim", + "bash", + "-lc", + CONTAINER_SCRIPT, + ]; +} + +// Runs INSIDE node:24-slim. Reads everything from env; polls with node's fetch +// (slim has no curl). Kept as a single quoted constant — no runtime interpolation. +export const CONTAINER_SCRIPT = ` +set -euo pipefail +echo "[verify-published] npm i -g omniroute@\${VERIFY_VERSION} (public registry)" +npm install -g "omniroute@\${VERIFY_VERSION}" +export DATA_DIR=/tmp/omniroute-data JWT_SECRET=verify-published-secret-with-sufficient-length API_KEY_SECRET=verify-published-api-key-secret DISABLE_SQLITE_AUTO_BACKUP=true OMNIROUTE_SKIP_SYSTEM_TRUST=1 +mkdir -p "\$DATA_DIR" +omniroute serve --port "\$VERIFY_PORT" & +node -e ' +const port = process.env.VERIFY_PORT; +const want = process.env.VERIFY_VERSION; +const deadline = Date.now() + Number(process.env.VERIFY_DEADLINE_S) * 1000; +(async () => { + while (Date.now() < deadline) { + try { + const res = await fetch("http://127.0.0.1:" + port + "/api/monitoring/health"); + const body = await res.json().catch(() => null); + if (res.status === 200 && body && body.version === want) { + console.log("[verify-published] healthy: HTTP 200, version " + body.version); + process.exit(0); + } + if (res.status === 200 && body && body.version !== want) { + console.error("[verify-published] WRONG VERSION: " + body.version + " (want " + want + ")"); + process.exit(1); + } + } catch {} + await new Promise((r) => setTimeout(r, 2000)); + } + console.error("[verify-published] deadline: server never became healthy"); + process.exit(1); +})(); +' +`; + +function main() { + const version = parseVersionArg(process.argv[2]); + if (!version) { + console.error("usage: node scripts/release/verify-published.mjs [--no-docker]"); + process.exit(2); + } + try { + execFileSync("docker", ["--version"], { stdio: "ignore" }); + } catch { + console.error("[verify-published] docker unavailable — this verifier requires a clean container"); + process.exit(2); + } + console.log(`[verify-published] clean-container verify of omniroute@${version}…`); + const r = spawnSync("docker", buildDockerArgs(version), { stdio: "inherit" }); + if (r.status === 0) { + console.log("[verify-published] ✅ the published package installs and boots"); + process.exit(0); + } + console.error(`[verify-published] ❌ FAILED (exit ${r.status}) — consider: npm deprecate omniroute@${version} ""`); + process.exit(1); +} + +import path from "node:path"; +const isDirectRun = + process.argv[1] && path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname); +if (isDirectRun) main(); diff --git a/skills/cli-skill-collector/SKILL.md b/skills/cli-skill-collector/SKILL.md new file mode 100644 index 0000000000..add237ad11 --- /dev/null +++ b/skills/cli-skill-collector/SKILL.md @@ -0,0 +1,421 @@ +--- +name: cli-skill-collector +description: "Detect installed CLI coding tools (Claude Code, Codex, Cursor, Copilot, Cline and more), search GitHub for matching agent skills, and install them to the detected tools via OmniRoute's built-in APIs." +--- + + +## Overview + +Detect installed CLI coding tools (Claude Code, Codex, Cursor, Copilot, Cline and more), search GitHub for matching agent skills, and install them to the detected tools via OmniRoute's built-in APIs. + +## Quick install + +```bash +npm install -g omniroute # or: npx omniroute +omniroute --version +``` + +## Subcommands + +### `autostart` + +**Example:** + +```bash +omniroute autostart +``` + +### `autostart enable` + +**Example:** + +```bash +omniroute autostart enable +``` + +### `autostart disable` + +**Example:** + +```bash +omniroute autostart disable +``` + +### `autostart toggle` + +**Example:** + +```bash +omniroute autostart toggle +``` + +### `autostart status` + +**Example:** + +```bash +omniroute autostart status +``` + +### `config` + +Show or update CLI tool configuration + +**Example:** + +```bash +omniroute config +``` + +### `config list` + +List all CLI tools and config status + +**Flags:** + +- `--json` + +**Example:** + +```bash +omniroute config list +``` + +### `config get ` + +Show current config for a tool + +**Flags:** + +- `--json` + +**Example:** + +```bash +omniroute config get +``` + +### `config set ` + +Write config for a tool + +**Flags:** + +- `--model ` +- `--non-interactive` +- `--yes` + +**Example:** + +```bash +omniroute config set +``` + +### `config validate ` + +Validate config format without writing + +**Flags:** + +- `--model ` +- `--json` + +**Example:** + +```bash +omniroute config validate +``` + +### `config opencode` + +Generate OpenCode config (alias for + +**Flags:** + +- `--model ` +- `--non-interactive` +- `--yes` + +**Example:** + +```bash +omniroute config opencode +``` + +### `config lang` + +**Example:** + +```bash +omniroute config lang +``` + +### `config get` + +**Flags:** + +- `--json` + +**Example:** + +```bash +omniroute config get +``` + +### `config set ` + +**Flags:** + +- `--force` + +**Example:** + +```bash +omniroute config set +``` + +### `config list` + +**Flags:** + +- `--json` + +**Example:** + +```bash +omniroute config list +``` + +### `env` + +Show and manage environment variables + +**Example:** + +```bash +omniroute env +``` + +### `env show` + +Show current environment variables + +**Flags:** + +- `--json` + +**Example:** + +```bash +omniroute env show +``` + +### `env get ` + +Get a single environment variable + +**Example:** + +```bash +omniroute env get +``` + +### `env set ` + +Set an environment variable (current session only) + +**Example:** + +```bash +omniroute env set +``` + +### `setup` + +**Flags:** + +- `--password ` +- `--add-provider` +- `--provider ` +- `--provider-name ` +- `--api-key ` +- `--default-model ` +- `--provider-base-url ` +- `--test-provider` +- `--non-interactive` +- `--list` + +**Example:** + +```bash +omniroute setup +``` + +### `update` + +**Flags:** + +- `--check` +- `--apply` +- `--changelog` +- `--dry-run` +- `--no-backup` +- `--yes` + +**Example:** + +```bash +omniroute update +``` + + + + +# /cli-skill-collector — Agent Skill Collector + +Discover and install agent skills for your coding CLI tools — all through OmniRoute's built-in APIs. + +This skill teaches you how to: + +1. **Detect** which coding CLIs are installed on this machine +2. **Search** GitHub for relevant agent skills (SKILL.md repos) +3. **Install** discovered skills to the detected coding tools + +No separate Skill Collector app needed — OmniRoute's own CLI detection + GitHub search handles everything. + +--- + +## Step 1 — Detect installed coding tools + +Query OmniRoute's CLI tool detection to find which coding agents are installed: + +```bash +curl -H "Authorization: Bearer $OMNIROUTE_API_KEY" http://localhost:20128/api/skills/collect/detect +``` + +This returns: + +- Every CLI tool in OmniRoute's catalog (`CLI_TOOL_IDS`: claude, codex, cursor, copilot, opencode, cline, kilocode, hermes, hermes-agent, openclaw, droid, continue, qwen, windsurf, devin, antigravity, etc.) +- Whether each is **installed** and **runnable** +- GitHub skills **matched** to your installed tools (scored by relevance) + +Example response: + +```json +{ + "tools": { + "codex": { "installed": true, "runnable": true, "command": "codex" }, + "claude": { "installed": true, "runnable": true, "command": "claude" }, + "cursor": { "installed": false, "runnable": false } + }, + "installedToolIds": ["codex", "claude"], + "matchedSkills": [ + { "toolId": "codex", "repo": "user/skill-codex-xxx", "score": 0.85, "stars": 120 }, + { "toolId": "claude", "repo": "user/claude-agent-rules", "score": 0.92, "stars": 340 } + ], + "totalSkills": 85 +} +``` + +--- + +## Step 2 — Review matched skills + +For each installed tool, the API returns relevant GitHub repos that contain SKILL.md or agent configuration files. Use the `score` field to prioritize: + +| Score | Recommendation | +| ----- | ----------------------------------------------- | +| 0.80+ | Excellent — well-maintained, high stars, active | +| 0.60+ | Good — relevant with decent quality | +| 0.40+ | Fair — may need review | +| <0.40 | Low quality — skip | + +You can also browse manually: + +```bash +curl -H "Authorization: Bearer $OMNIROUTE_API_KEY" \ + "http://localhost:20128/api/github-skills?minStars=3&maxResults=50" +``` + +--- + +## Step 3 — Install skills to detected tools + +Install a chosen skill to one or more detected tools: + +```bash +curl -X POST http://localhost:20128/api/skills/collect/install \ + -H "Authorization: Bearer $OMNIROUTE_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "repoName": "user/skill-codex-xxx", + "targets": ["codex", "claude"], + "description": "Agent skill for coding workflows" + }' +``` + +This plans the installation path for each target tool: + +- **claude** → `~/.claude/skills/{category}/` +- **codex** → `~/.codex/skills/{category}/` +- **hermes** → `~/AppData/Local/hermes/skills/{category}/` +- **opencode** → `~/.opencode/skills/{category}/` +- **gemini** → `~/.gemini/skills/{category}/` + +The actual file sync (cloning from GitHub and copying SKILL.md) is done by the agent using standard `curl` + `cp` commands. + +--- + +## Step 4 — Verify installation + +After installing, verify the skill is in place: + +```bash +# For Codex +ls -la ~/.codex/skills/imported-github/*/SKILL.md + +# For Claude Code +ls -la ~/.claude/skills/imported-github/*/SKILL.md + +# For Hermes (Windows) +ls -la ~/AppData/Local/hermes/skills/imported-github/*/SKILL.md +``` + +Also re-check detection: + +```bash +curl -H "Authorization: Bearer $OMNIROUTE_API_KEY" http://localhost:20128/api/skills/collect/detect +``` + +--- + +## Quick start (full workflow) + +```bash +AUTH_HEADER="Authorization: Bearer $OMNIROUTE_API_KEY" + +# 1. Detect +DETECT=$(curl -s -H "$AUTH_HEADER" http://localhost:20128/api/skills/collect/detect) + +# 2. Pick top matched skill for first installed tool +TOOL=$(echo "$DETECT" | python3 -c "import sys,json;d=json.load(sys.stdin);print(d['installedToolIds'][0] if d['installedToolIds'] else '')") +SKILL=$(echo "$DETECT" | python3 -c "import sys,json;d=json.load(sys.stdin);ms=d.get('matchedSkills',[]);print(ms[0]['repo'] if ms else '')") + +if [ -n "$TOOL" ] && [ -n "$SKILL" ]; then + # 3. Install + curl -s -X POST http://localhost:20128/api/skills/collect/install \ + -H "$AUTH_HEADER" \ + -H "Content-Type: application/json" \ + -d "{\"repoName\": \"$SKILL\", \"targets\": [\"$TOOL\"]}" + echo "Installed $SKILL to $TOOL" +fi +``` + +--- + +## Notes + +- OmniRoute must be running locally on port 20128 (default) — see `docs/frameworks/SKILLS.md` for custom-port setups. +- The `/api/skills/collect/*` and `/api/github-skills` endpoints require **management-scoped authentication** the same way every other `/api/skills/*` route does: a dashboard session, the loopback CLI token, or an API key with the `manage` scope (`requireManagementAuth()`). Auth is only bypassed when the server has no login/API-key requirement configured at all. +- This replaces the standalone Skill Collector Python app — all logic is now inside OmniRoute. + diff --git a/src/app/(dashboard)/dashboard/cli-code/components/HermesAgentToolCard.tsx b/src/app/(dashboard)/dashboard/cli-code/components/HermesAgentToolCard.tsx index c7d356ee36..a16d5f9998 100644 --- a/src/app/(dashboard)/dashboard/cli-code/components/HermesAgentToolCard.tsx +++ b/src/app/(dashboard)/dashboard/cli-code/components/HermesAgentToolCard.tsx @@ -47,6 +47,10 @@ export default function HermesAgentToolCard({ const [previewYaml, setPreviewYaml] = useState(null); const [isPreviewLoading, setIsPreviewLoading] = useState(false); const [firstSetupAt, setFirstSetupAt] = useState(null); + // Model aliases drive the passthrough provider groups (OpenRouter, Requesty, + // DGrid, AgentRouter, Charm Hyper, ...) in ModelSelectModal — without them, + // those providers never surface in the Hermes Agent role picker (#7151). + const [modelAliases, setModelAliases] = useState({}); // Track whether we have already seeded from batchStatus on this expand const seededFromBatchRef = useRef(false); @@ -109,8 +113,19 @@ export default function HermesAgentToolCard({ }); } loadCurrentConfig(); + fetchModelAliases(); }, [isExpanded, batchStatus, loadCurrentConfig]); + const fetchModelAliases = async () => { + try { + const res = await fetch("/api/models/alias"); + const data = await res.json(); + if (res.ok) setModelAliases(data.aliases || {}); + } catch (error) { + console.warn("Error fetching model aliases:", error); + } + }; + const setRoleSelection = (roleId: string, model: string, provider = "OmniRoute") => { setSelections((prev) => ({ ...prev, [roleId]: { model, provider } })); }; @@ -522,6 +537,7 @@ export default function HermesAgentToolCard({ showCombos={true} activeProviders={activeProviders} alwaysIncludeProviders={HERMES_AGENT_ZERO_CONFIG_PROVIDERS} + modelAliases={modelAliases} /> ); diff --git a/src/app/(dashboard)/dashboard/combos/page.tsx b/src/app/(dashboard)/dashboard/combos/page.tsx index a91ac0c7f3..6857fbd79a 100644 --- a/src/app/(dashboard)/dashboard/combos/page.tsx +++ b/src/app/(dashboard)/dashboard/combos/page.tsx @@ -14,6 +14,7 @@ import Toggle from "@/shared/components/Toggle"; import Tooltip from "@/shared/components/Tooltip"; import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; import { FieldLabelWithHelp, WeightTotalBar } from "./parts"; +import { useComboProxyAssignments } from "./useComboProxyAssignments"; import { ResponseValidationEditor, type ResponseValidationValue } from "./ResponseValidationEditor"; import ReasoningTokenBufferToggle from "./ReasoningTokenBufferToggle"; import { pickDisplayValue } from "@/shared/utils/maskEmail"; @@ -681,6 +682,7 @@ export default function CombosPage() { const notify = useNotificationStore(); const [proxyTargetCombo, setProxyTargetCombo] = useState(null); const [proxyConfig, setProxyConfig] = useState(null); + const { comboProxyAssignedIds, fetchComboProxyAssignments } = useComboProxyAssignments(); const [providerNodes, setProviderNodes] = useState([]); const [showUsageGuide, setShowUsageGuide] = useState(true); const [recentlyCreatedCombo, setRecentlyCreatedCombo] = useState(""); @@ -1210,7 +1212,7 @@ export default function CombosPage() { onTest={() => handleTestCombo(combo)} testing={testingCombo === combo.name} onProxy={() => setProxyTargetCombo(combo)} - hasProxy={!!proxyConfig?.combos?.[combo.id]} + hasProxy={comboProxyAssignedIds.has(combo.id) || !!proxyConfig?.combos?.[combo.id]} onToggle={() => handleToggleCombo(combo)} dragDisabled={savingComboOrder || activeFilter !== "all" || combos.length < 2} isDragged={comboDragIndex === index} @@ -1260,7 +1262,7 @@ export default function CombosPage() { {proxyTargetCombo && ( setProxyTargetCombo(null)} + onClose={() => (setProxyTargetCombo(null), fetchComboProxyAssignments())} level="combo" levelId={proxyTargetCombo.id} levelLabel={proxyTargetCombo.name} diff --git a/src/app/(dashboard)/dashboard/combos/useComboProxyAssignments.ts b/src/app/(dashboard)/dashboard/combos/useComboProxyAssignments.ts new file mode 100644 index 0000000000..93a9a0a5a0 --- /dev/null +++ b/src/app/(dashboard)/dashboard/combos/useComboProxyAssignments.ts @@ -0,0 +1,33 @@ +import { useCallback, useEffect, useState } from "react"; + +// #7149: the Combo "Set Proxy" modal writes through the modern proxy_assignments +// registry (scope="combo"), not the legacy /api/settings/proxy `combos` map — the +// dashboard's "has a proxy" indicator must read from the same registry the modal +// actually writes to, or it stays stale/gray even after a successful save. +export function parseComboProxyAssignmentIds(data: unknown): string[] { + const items = (data as { items?: unknown })?.items; + if (!Array.isArray(items)) return []; + return items + .filter( + (entry): entry is { scopeId: string; proxyId: string } => + !!(entry as { scopeId?: unknown })?.scopeId && !!(entry as { proxyId?: unknown })?.proxyId + ) + .map((entry) => entry.scopeId); +} + +export function useComboProxyAssignments() { + const [comboProxyAssignedIds, setComboProxyAssignedIds] = useState>(new Set()); + + const fetchComboProxyAssignments = useCallback(() => { + fetch("/api/settings/proxies/assignments?scope=combo") + .then((r) => (r.ok ? r.json() : null)) + .then((data) => setComboProxyAssignedIds(new Set(parseComboProxyAssignmentIds(data)))) + .catch(() => {}); + }, []); + + useEffect(() => { + fetchComboProxyAssignments(); + }, [fetchComboProxyAssignments]); + + return { comboProxyAssignedIds, fetchComboProxyAssignments }; +} diff --git a/src/app/(dashboard)/dashboard/onboarding/components/TierFlowDiagram.tsx b/src/app/(dashboard)/dashboard/onboarding/components/TierFlowDiagram.tsx index d2cc88ad13..ded42fdf0d 100644 --- a/src/app/(dashboard)/dashboard/onboarding/components/TierFlowDiagram.tsx +++ b/src/app/(dashboard)/dashboard/onboarding/components/TierFlowDiagram.tsx @@ -5,7 +5,8 @@ import { useTranslations } from "next-intl"; import Image from "next/image"; export function TierFlowDiagram() { - const t = useTranslations("onboarding"); + const t = useTranslations("onboarding.tier"); + const tOnboarding = useTranslations("onboarding"); const { resolvedTheme } = useTheme(); const src = resolvedTheme === "dark" ? "/images/tier-flow-dark.svg" : "/images/tier-flow-light.svg"; @@ -14,15 +15,14 @@ export function TierFlowDiagram() {
{t("tierFlowDiagramAlt")} -

- Requests flow through your subscription quotas first, then pay-per-token cheap providers, - then free-tier providers — automatic, zero-config. +

+ {t("flowCaption")}

); diff --git a/src/app/(dashboard)/dashboard/onboarding/page.tsx b/src/app/(dashboard)/dashboard/onboarding/page.tsx index a1cbf3544e..de8b44e3cf 100644 --- a/src/app/(dashboard)/dashboard/onboarding/page.tsx +++ b/src/app/(dashboard)/dashboard/onboarding/page.tsx @@ -274,7 +274,12 @@ export default function OnboardingWizard() { > {currentStep.icon} -

{currentStep.title}

+

{currentStep.title}

+ {currentStep.id === "tiers" && ( +

+ {t("tier.subtitle")} +

+ )} {/* Step Content */} @@ -283,7 +288,7 @@ export default function OnboardingWizard() { {currentStep.id === "welcome" && (

{t("welcomeDesc")}

-
+
{[ { icon: "swap_horiz", label: t("multiProvider") }, { icon: "monitoring", label: t("usageTracking") }, @@ -291,12 +296,14 @@ export default function OnboardingWizard() { ].map((f) => (
- - {f.icon} - - {f.label} +
+ + {f.icon} + + {f.label} +
))}
diff --git a/src/app/(dashboard)/dashboard/onboarding/steps/TierTour.tsx b/src/app/(dashboard)/dashboard/onboarding/steps/TierTour.tsx index 02f0d3d286..c09276df3d 100644 --- a/src/app/(dashboard)/dashboard/onboarding/steps/TierTour.tsx +++ b/src/app/(dashboard)/dashboard/onboarding/steps/TierTour.tsx @@ -19,7 +19,7 @@ function TierCard({ number, colorClass, label, description, examples }: TierCard {number} {label}
-

{description}

+

{description}

    {examples.map((e) => (
  • · {e}
  • @@ -34,10 +34,6 @@ export function TierTour() { return (
    -
    -

    {t("subtitle")}

    -
    -
    @@ -68,7 +64,7 @@ export function TierTour() { {t("configure")} {" "} - after setup. + {t("afterSetup")}

    ); diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/CustomModelsSection.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/CustomModelsSection.tsx index 76b861739d..dd2e842159 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/CustomModelsSection.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/CustomModelsSection.tsx @@ -98,6 +98,11 @@ export default function CustomModelsSection({ // #4125: manual context-window override (Feature 5004 table) — free text so the // field can be left blank (no override) without fighting a number input's "0". const [editingContextWindowOverride, setEditingContextWindowOverride] = useState(""); + // #1904: manual vision-capability override — some self-hosted/local OpenAI-compatible + // backends don't self-report an image input modality, so the user needs a way to flag + // the model as vision-capable by hand (read back by getCustomVisionCapabilityFields()). + const [newSupportsVision, setNewSupportsVision] = useState(false); + const [editingSupportsVision, setEditingSupportsVision] = useState(false); const customMap = useMemo(() => buildCompatMap(customModels), [customModels]); const overrideMap = useMemo(() => buildCompatMap(modelCompatOverrides), [modelCompatOverrides]); @@ -135,6 +140,7 @@ export default function CustomModelsSection({ apiFormat: newApiFormat, supportedEndpoints: newEndpoints, ...(newTargetFormat ? { targetFormat: newTargetFormat } : {}), + ...(newSupportsVision ? { supportsVision: true } : {}), }), }); if (res.ok) { @@ -143,6 +149,7 @@ export default function CustomModelsSection({ setNewApiFormat("chat-completions"); setNewEndpoints(["chat"]); setNewTargetFormat(""); + setNewSupportsVision(false); await fetchCustomModels(); onModelsChanged?.(); } @@ -202,6 +209,7 @@ export default function CustomModelsSection({ setEditingContextWindowOverride( typeof model.contextWindowOverride === "number" ? String(model.contextWindowOverride) : "" ); + setEditingSupportsVision(model.supportsVision === true); }; const cancelEdit = () => { @@ -210,6 +218,7 @@ export default function CustomModelsSection({ setEditingEndpoints(["chat"]); setEditingTargetFormat(""); setEditingContextWindowOverride(""); + setEditingSupportsVision(false); setSavingModelId(null); }; @@ -268,6 +277,9 @@ export default function CustomModelsSection({ ...(editingTargetFormat ? { targetFormat: editingTargetFormat } : {}), // #4125: manual context-window override — number to set, null to clear. contextWindowOverride, + // #1904: manual vision-capability override — true/false to set, null to + // clear back to the id-based heuristic. + supportsVision: editingSupportsVision ? true : null, }), }); @@ -425,6 +437,23 @@ export default function CustomModelsSection({ ))}
+
+   + +
@@ -482,6 +511,14 @@ export default function CustomModelsSection({ {`🪟 ${model.contextWindowOverride.toLocaleString()}`} )} + {model.supportsVision === true && ( + + {`👁️ ${t("visionCapableLabel")}`} + + )} {model.supportedEndpoints?.includes("embeddings") && ( {`📐 ${t("supportedEndpointEmbeddings")}`} @@ -578,6 +615,23 @@ export default function CustomModelsSection({ className="w-full px-2.5 py-2 text-xs border border-border rounded-lg bg-background text-text-main focus:outline-none focus:border-primary" /> +
+ + +
{t("supportedEndpointsLabel")} diff --git a/src/app/(dashboard)/dashboard/providers/[id]/providerCredentialText.ts b/src/app/(dashboard)/dashboard/providers/[id]/providerCredentialText.ts new file mode 100644 index 0000000000..6c2c20026d --- /dev/null +++ b/src/app/(dashboard)/dashboard/providers/[id]/providerCredentialText.ts @@ -0,0 +1,132 @@ +// Pure, shared helpers for provider credential copy (labels/hints/titles for +// the API-key and web-session-credential modals). Extracted out of +// providerPageHelpers.ts (Issue #3501 strangler-fig decomposition) — that leaf +// is frozen at its file-size cap, so this cohesive slice (message-translation +// utility + the 4 web-session-credential text builders) lives here instead and +// is re-exported from providerPageHelpers.ts for backward compatibility. Leaf +// module — imports only from @/shared, @/lib and colocated sibling modules. +import { type WebSessionCredentialRequirement } from "./webSessionCredentials"; + +export type ProviderMessageTranslator = (( + key: string, + values?: Record +) => string) & { + has?: (key: string) => boolean; +}; + +export function providerText( + t: ProviderMessageTranslator, + key: string, + fallback: string, + values?: Record +): string { + if (typeof t.has === "function" && t.has(key)) { + return t(key, values); + } + if (values) { + return Object.entries(values).reduce( + (acc, [name, value]) => acc.replaceAll(`{${name}}`, String(value)), + fallback + ); + } + return fallback; +} + +export function getWebSessionCredentialLabel( + t: ProviderMessageTranslator, + requirement: WebSessionCredentialRequirement, + optional: boolean +): string { + if (requirement.kind === "none") { + return providerText(t, "webNoAuthCredentialLabel", "No credential required"); + } + const baseLabel = + requirement.kind === "token" + ? providerText(t, "webTokenCredentialLabel", "Web session token") + : t("sessionCookieLabel"); + return optional ? `${baseLabel} (${t("optional").toLowerCase()})` : baseLabel; +} + +export function getWebSessionCredentialHint( + t: ProviderMessageTranslator, + requirement: WebSessionCredentialRequirement, + providerName: string, + editing: boolean +): string | undefined { + if (requirement.kind === "none") return undefined; + + const values = { provider: providerName, credential: requirement.credentialName }; + if (editing) { + return requirement.kind === "token" + ? providerText( + t, + "webTokenEditHint", + "Leave blank to keep the current web session token. Credential: {credential}.", + values + ) + : providerText( + t, + "webCookieEditHint", + "Leave blank to keep the current session cookie. Required cookie: {credential}.", + values + ); + } + + // #5465 — a provider-specific hint (e.g. t3.chat's step-by-step DevTools copy) + // replaces the generic one-line cookie/token template when that template is + // unclear for the provider (t3.chat needs a localStorage value AND the Cookie + // header, so "Required cookie: convex-session-id + Cookie header…" reads + // circular). The override key ships translated in every locale. + if (requirement.hintKey) { + return providerText( + t, + requirement.hintKey, + requirement.hintFallback ?? + "Open the provider's web session in DevTools, copy the required credential(s), and paste them in the fields below.", + values + ); + } + + return requirement.kind === "token" + ? providerText( + t, + "webTokenCredentialHint", + "Credential: {credential}. Paste the token value from your own signed-in {provider} web session, or a DevTools HAR export if the provider supports it.", + values + ) + : providerText( + t, + "webCookieCredentialHint", + "Required cookie: {credential}. Paste the Cookie header value from your own signed-in {provider} web session. Do not include the Cookie: prefix.", + values + ); +} + +export function getWebSessionCredentialCheckLabel( + t: ProviderMessageTranslator, + requirement: WebSessionCredentialRequirement +): string { + if (requirement.kind === "token") return providerText(t, "checkWebToken", "Check token"); + return providerText(t, "checkCookie", "Check cookie"); +} + +export function getAddCredentialModalTitle( + t: ProviderMessageTranslator, + providerName: string, + requirement: WebSessionCredentialRequirement | null +): string { + if (!requirement) return t("addProviderApiKeyTitle", { provider: providerName }); + if (requirement.kind === "none") { + return providerText(t, "addProviderConnectionTitle", "Add {provider} connection", { + provider: providerName, + }); + } + if (requirement.kind === "token") { + return providerText(t, "addProviderWebTokenTitle", "Add {provider} web token", { + provider: providerName, + }); + } + return providerText(t, "addProviderSessionCookieTitle", "Add {provider} session cookie", { + provider: providerName, + }); +} diff --git a/src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts b/src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts index 4345d11962..22a25dc59f 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts +++ b/src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts @@ -16,20 +16,32 @@ import { type CodexServiceTier, } from "@/lib/providers/requestDefaults"; import { type CodexGlobalServiceMode } from "@/lib/providers/codexFastTier"; -import { type WebSessionCredentialRequirement } from "./webSessionCredentials"; import { CC_COMPATIBLE_DEFAULT_CHAT_PATH } from "./providerDetailConstants"; +import { + type ProviderMessageTranslator, + providerText, + getWebSessionCredentialLabel, + getWebSessionCredentialHint, + getWebSessionCredentialCheckLabel, + getAddCredentialModalTitle, +} from "./providerCredentialText"; + +// Re-exported for backward compatibility — these used to be defined here +// (Issue #3501 strangler-fig home), but were extracted to providerCredentialText.ts +// once this leaf hit its frozen file-size cap (#1904 own growth). +export { + type ProviderMessageTranslator, + providerText, + getWebSessionCredentialLabel, + getWebSessionCredentialHint, + getWebSessionCredentialCheckLabel, + getAddCredentialModalTitle, +}; // --------------------------------------------------------------------------- // Types shared between page + modals // --------------------------------------------------------------------------- -export type ProviderMessageTranslator = (( - key: string, - values?: Record -) => string) & { - has?: (key: string) => boolean; -}; - export type LocalProviderMetadata = { name?: string; localDefault?: string; @@ -76,6 +88,9 @@ export type CompatModelRow = { compatByProtocol?: CompatByProtocolMap; /** #2905: per-model upstream wire-format override. */ targetFormat?: string; /** #4125: manual context-window override (tokens), when set. */ contextWindowOverride?: number; + /** #1904: manual vision-capability override for custom models whose upstream + * discovery metadata doesn't self-report an image input modality. */ + supportsVision?: boolean; }; export type CompatModelMap = Map; @@ -98,28 +113,6 @@ export function targetFormatBadgeI18nKey(value: string): string | null { return TARGET_FORMAT_BADGE_I18N_KEYS[value] ?? null; } -// --------------------------------------------------------------------------- -// Utility — message translation with fallback -// --------------------------------------------------------------------------- - -export function providerText( - t: ProviderMessageTranslator, - key: string, - fallback: string, - values?: Record -): string { - if (typeof t.has === "function" && t.has(key)) { - return t(key, values); - } - if (values) { - return Object.entries(values).reduce( - (acc, [name, value]) => acc.replaceAll(`{${name}}`, String(value)), - fallback - ); - } - return fallback; -} - /** #5442 — badge for add-credential validation; unsupported → neutral N/A (not red Invalid). */ export function validationBadgeProps(result: string): { variant: "success" | "error" | "info"; @@ -385,108 +378,10 @@ export function formatExcludedModelsInput(value: unknown): string { } // --------------------------------------------------------------------------- -// Web-session credential label / hint helpers (Phase 2b) +// Web-session credential label / hint helpers (Phase 2b) — moved to +// providerCredentialText.ts (#1904 own growth); re-exported above. // --------------------------------------------------------------------------- -export function getWebSessionCredentialLabel( - t: ProviderMessageTranslator, - requirement: WebSessionCredentialRequirement, - optional: boolean -): string { - if (requirement.kind === "none") { - return providerText(t, "webNoAuthCredentialLabel", "No credential required"); - } - const baseLabel = - requirement.kind === "token" - ? providerText(t, "webTokenCredentialLabel", "Web session token") - : t("sessionCookieLabel"); - return optional ? `${baseLabel} (${t("optional").toLowerCase()})` : baseLabel; -} - -export function getWebSessionCredentialHint( - t: ProviderMessageTranslator, - requirement: WebSessionCredentialRequirement, - providerName: string, - editing: boolean -): string | undefined { - if (requirement.kind === "none") return undefined; - - const values = { provider: providerName, credential: requirement.credentialName }; - if (editing) { - return requirement.kind === "token" - ? providerText( - t, - "webTokenEditHint", - "Leave blank to keep the current web session token. Credential: {credential}.", - values - ) - : providerText( - t, - "webCookieEditHint", - "Leave blank to keep the current session cookie. Required cookie: {credential}.", - values - ); - } - - // #5465 — a provider-specific hint (e.g. t3.chat's step-by-step DevTools copy) - // replaces the generic one-line cookie/token template when that template is - // unclear for the provider (t3.chat needs a localStorage value AND the Cookie - // header, so "Required cookie: convex-session-id + Cookie header…" reads - // circular). The override key ships translated in every locale. - if (requirement.hintKey) { - return providerText( - t, - requirement.hintKey, - requirement.hintFallback ?? - "Open the provider's web session in DevTools, copy the required credential(s), and paste them in the fields below.", - values - ); - } - - return requirement.kind === "token" - ? providerText( - t, - "webTokenCredentialHint", - "Credential: {credential}. Paste the token value from your own signed-in {provider} web session, or a DevTools HAR export if the provider supports it.", - values - ) - : providerText( - t, - "webCookieCredentialHint", - "Required cookie: {credential}. Paste the Cookie header value from your own signed-in {provider} web session. Do not include the Cookie: prefix.", - values - ); -} - -export function getWebSessionCredentialCheckLabel( - t: ProviderMessageTranslator, - requirement: WebSessionCredentialRequirement -): string { - if (requirement.kind === "token") return providerText(t, "checkWebToken", "Check token"); - return providerText(t, "checkCookie", "Check cookie"); -} - -export function getAddCredentialModalTitle( - t: ProviderMessageTranslator, - providerName: string, - requirement: WebSessionCredentialRequirement | null -): string { - if (!requirement) return t("addProviderApiKeyTitle", { provider: providerName }); - if (requirement.kind === "none") { - return providerText(t, "addProviderConnectionTitle", "Add {provider} connection", { - provider: providerName, - }); - } - if (requirement.kind === "token") { - return providerText(t, "addProviderWebTokenTitle", "Add {provider} web token", { - provider: providerName, - }); - } - return providerText(t, "addProviderSessionCookieTitle", "Add {provider} session cookie", { - provider: providerName, - }); -} - // --------------------------------------------------------------------------- // Upstream-headers helpers (Phase 2b) // --------------------------------------------------------------------------- diff --git a/src/app/(dashboard)/dashboard/providers/services/components/CliproxyProviderExposureCard.tsx b/src/app/(dashboard)/dashboard/providers/services/components/CliproxyProviderExposureCard.tsx new file mode 100644 index 0000000000..d3e406747a --- /dev/null +++ b/src/app/(dashboard)/dashboard/providers/services/components/CliproxyProviderExposureCard.tsx @@ -0,0 +1,89 @@ +/** + * Provider Exposure toggle for CLIProxyAPI. + * Persists the `providerExpose` field via POST /api/services/cliproxy/provider-expose. + * When enabled, CLIProxyAPI models appear as `cliproxyapi/...` in OmniRoute's model selection. + */ +"use client"; + +import { useState } from "react"; +import { Card, Toggle } from "@/shared/components"; +import { useServiceStatus } from "../hooks/useServiceStatus"; + +const NAME = "cliproxy"; + +export function CliproxyProviderExposureCard() { + const { data, mutate } = useServiceStatus(NAME); + const [pending, setPending] = useState(false); + const [msg, setMsg] = useState<{ ok: boolean; text: string } | null>(null); + + async function handleToggle(enabled: boolean) { + setPending(true); + setMsg(null); + try { + const res = await fetch(`/api/services/${NAME}/provider-expose`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ enabled }), + }); + if (!res.ok) { + const body = await res.json().catch(() => ({})); + const errorMsg = + body?.error?.message ?? body?.message ?? `Failed to update (HTTP ${res.status})`; + setMsg({ ok: false, text: errorMsg }); + return; + } + setMsg(null); + mutate(); + } catch { + setMsg({ ok: false, text: "Network error — could not update provider exposure setting" }); + } finally { + setPending(false); + } + } + + return ( + +
+
+ hub +
+
+

Provider Exposure

+

+ Expose CLIProxyAPI models as a routing target under the{" "} + cliproxyapi/ prefix. +

+
+
+ + {msg && ( +
+ + {msg.ok ? "check_circle" : "error"} + + {msg.text} +
+ )} + +
+
+

+ Expose as{" "} + cliproxyapi/... +

+

+ When enabled, discovered models appear in provider selects across OmniRoute. +

+
+ +
+
+ ); +} + diff --git a/src/app/(dashboard)/dashboard/providers/services/tabs/CliproxyServiceTab.tsx b/src/app/(dashboard)/dashboard/providers/services/tabs/CliproxyServiceTab.tsx index 6d7245e21a..a25686f292 100644 --- a/src/app/(dashboard)/dashboard/providers/services/tabs/CliproxyServiceTab.tsx +++ b/src/app/(dashboard)/dashboard/providers/services/tabs/CliproxyServiceTab.tsx @@ -6,6 +6,7 @@ import { ServiceLogsPanel } from "../components/ServiceLogsPanel"; import { CliproxyModelMappingEditor } from "../components/CliproxyModelMappingEditor"; import { AutoStartToggle } from "../components/AutoStartToggle"; import { CliproxyConnectionPanel } from "../components/CliproxyConnectionPanel"; +import { CliproxyProviderExposureCard } from "../components/CliproxyProviderExposureCard"; const NAME = "cliproxy"; @@ -19,6 +20,7 @@ export function CliproxyServiceTab() { description="Launch CLIProxyAPI automatically when OmniRoute starts" /> +
diff --git a/src/app/(dashboard)/dashboard/search-tools/components/tabs/CompareTab.tsx b/src/app/(dashboard)/dashboard/search-tools/components/tabs/CompareTab.tsx index a9165a7684..8b702028cd 100644 --- a/src/app/(dashboard)/dashboard/search-tools/components/tabs/CompareTab.tsx +++ b/src/app/(dashboard)/dashboard/search-tools/components/tabs/CompareTab.tsx @@ -5,6 +5,8 @@ import { useTranslations } from "next-intl"; import Link from "next/link"; import type { SearchProviderCatalogItem } from "@/shared/schemas/searchTools"; +const MAX_COMPARE_PROVIDERS = 4; // D22: cap at 4 providers running in parallel + export interface CompareResult { provider: string; latency: number; @@ -69,12 +71,15 @@ export default function CompareTab({ providers, onMetrics }: CompareTabProps) { const toggleProvider = useCallback((id: string) => { setSelectedProviderIds((prev) => { if (prev.includes(id)) return prev.filter((p) => p !== id); + if (prev.length >= MAX_COMPARE_PROVIDERS) return prev; return [...prev, id]; }); }, []); const selectAll = useCallback(() => { - setSelectedProviderIds(activeSearchProviders.map((p) => p.id)); + setSelectedProviderIds( + activeSearchProviders.slice(0, MAX_COMPARE_PROVIDERS).map((p) => p.id) + ); }, [activeSearchProviders]); const clearAll = useCallback(() => { @@ -230,7 +235,7 @@ export default function CompareTab({ providers, onMetrics }: CompareTabProps) { - {/* Provider picker — no cap */} + {/* Provider picker — capped at MAX_COMPARE_PROVIDERS (D22) */}

@@ -253,9 +258,15 @@ export default function CompareTab({ providers, onMetrics }: CompareTabProps) {

+ {selectedProviderIds.length >= MAX_COMPARE_PROVIDERS && ( +

+ Maximum of {MAX_COMPARE_PROVIDERS} providers can be compared at once. +

+ )}
{activeSearchProviders.map((p) => { const selected = selectedProviderIds.includes(p.id); + const atCap = !selected && selectedProviderIds.length >= MAX_COMPARE_PROVIDERS; return ( . +setup("autentica e salva storageState", async ({ page }) => { + await page.goto("/login"); + await page.locator('input[type="password"]').fill(process.env.HOMOLOG_ADMIN_PASSWORD!); + await page.locator('button[type="submit"]').click(); + await page.waitForURL(/\/dashboard/); + await expect(page).toHaveURL(/dashboard/); + await page.context().storageState({ path: STORAGE_STATE }); +}); diff --git a/tests/homolog/ui/playwright.config.ts b/tests/homolog/ui/playwright.config.ts new file mode 100644 index 0000000000..78e099debe --- /dev/null +++ b/tests/homolog/ui/playwright.config.ts @@ -0,0 +1,42 @@ +import { defineConfig } from "@playwright/test"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const HERE = path.dirname(fileURLToPath(import.meta.url)); +export const STORAGE_STATE = path.join(HERE, ".auth", "admin.json"); + +export default defineConfig({ + testDir: ".", + timeout: 60_000, + retries: 1, + // Sem fullyParallel, os 98 testes de routes.spec.ts (mesmo arquivo) rodam + // SERIALIZADOS num único worker (~10min); com ele, distribuem entre os workers. + fullyParallel: true, + workers: 8, + reporter: [ + ["list"], + [ + // outputDir ABSOLUTO: o reporter resolve paths relativos contra o CWD do + // processo (não contra o config) — um path relativo escapava do worktree. + "playwright-ctrf-json-reporter", + { + outputDir: path.resolve(HERE, "..", "..", "..", "homolog-report"), + outputFile: "ui-ctrf.json", + }, + ], + ], + use: { + baseURL: process.env.HOMOLOG_BASE_URL || "http://192.168.0.15:20128", + trace: "retain-on-failure", + screenshot: "only-on-failure", + }, + projects: [ + { name: "setup", testMatch: /auth\.setup\.ts/ }, + { + name: "homolog", + testMatch: /.*\.spec\.ts/, + dependencies: ["setup"], + use: { storageState: STORAGE_STATE }, + }, + ], +}); diff --git a/tests/homolog/ui/routes.spec.ts b/tests/homolog/ui/routes.spec.ts new file mode 100644 index 0000000000..febc68749c --- /dev/null +++ b/tests/homolog/ui/routes.spec.ts @@ -0,0 +1,40 @@ +import { test, expect } from "@playwright/test"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +// Descobre as rotas estáticas do dashboard a partir do próprio repo: +// cada page.tsx sob src/app/(dashboard)/dashboard vira uma rota; grupos (x) somem +// do path e rotas dinâmicas [param] são puladas (sem dado real garantido). +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../.."); +const BASE = path.join(ROOT, "src", "app", "(dashboard)", "dashboard"); + +function discoverRoutes(dir: string, prefix = "/dashboard"): string[] { + const routes: string[] = []; + if (fs.existsSync(path.join(dir, "page.tsx"))) routes.push(prefix || "/"); + for (const e of fs.readdirSync(dir, { withFileTypes: true })) { + if (!e.isDirectory() || e.name.startsWith("[") || e.name.startsWith("_")) continue; + const seg = e.name.startsWith("(") ? "" : `/${e.name}`; + routes.push(...discoverRoutes(path.join(dir, e.name), `${prefix}${seg}`)); + } + return [...new Set(routes)]; +} + +for (const route of discoverRoutes(BASE)) { + test(`rota ${route} carrega sem crash`, async ({ page }) => { + const pageErrors: string[] = []; + page.on("pageerror", (err) => pageErrors.push(err.message)); + + const res = await page.goto(route, { waitUntil: "domcontentloaded" }); + expect(res!.status(), `HTTP em ${route}`).toBeLessThan(400); + // "networkidle" nunca assenta em telas com polling/websocket ao vivo (30s x 98 rotas + // estourava o run inteiro) — "load" + um settle curto e suficiente para hidratar e + // deixar um crash de client component (pageerror / error boundary) aparecer. + await page.waitForLoadState("load", { timeout: 10_000 }).catch(() => {}); + await page.waitForTimeout(1_500); + + // Error boundary do Next: nunca pode aparecer + await expect(page.locator("text=Application error")).toHaveCount(0); + expect(pageErrors, `pageerror em ${route}: ${pageErrors.join(" | ")}`).toHaveLength(0); + }); +} diff --git a/tests/integration/agent-skills-content.test.ts b/tests/integration/agent-skills-content.test.ts index 9e0ad388ab..74eed7741b 100644 --- a/tests/integration/agent-skills-content.test.ts +++ b/tests/integration/agent-skills-content.test.ts @@ -2,9 +2,9 @@ * Integration tests for Agent Skills content integrity. * * Verifies: - * 1. All 43 skill IDs from catalog have skills/{id}/ folder with SKILL.md. + * 1. All 44 skill IDs from catalog have skills/{id}/ folder with SKILL.md. * 2. Zero omniroute-* folders remain (post-prune: old omniroute-* skill dirs were removed). - * 3. 10 specific IDs have ... blocks: + * 3. 12 specific IDs have ... blocks: * omni-mcp, omni-compression, cli-providers, cli-eval, omni-agents-a2a, * omni-combos-routing, omni-auth, omni-resilience, omni-inference, cli-serve. * @@ -22,6 +22,7 @@ const ALL_IDS = [...API_SKILL_IDS, ...CLI_SKILL_IDS, ...CONFIG_SKILL_IDS] as str // IDs that must have a custom block const CUSTOM_BLOCK_IDS = [ + "cli-skill-collector", "omni-mcp", "omni-compression", "cli-providers", @@ -37,7 +38,7 @@ const CUSTOM_BLOCK_IDS = [ // ── §1: All 42 catalog IDs have skills/{id}/SKILL.md ───────────────────────── -test("all 43 catalog IDs have a skills/{id}/ directory", () => { +test("all 44 catalog IDs have a skills/{id}/ directory", () => { const missing: string[] = []; for (const id of ALL_IDS) { const dirPath = path.join(SKILLS_DIR, id); @@ -48,7 +49,7 @@ test("all 43 catalog IDs have a skills/{id}/ directory", () => { assert.deepEqual(missing, [], `Missing skill directories: ${missing.join(", ")}`); }); -test("all 43 catalog IDs have a skills/{id}/SKILL.md file", () => { +test("all 44 catalog IDs have a skills/{id}/SKILL.md file", () => { const missing: string[] = []; for (const id of ALL_IDS) { const skillPath = path.join(SKILLS_DIR, id, "SKILL.md"); @@ -113,7 +114,7 @@ for (const id of CUSTOM_BLOCK_IDS) { // ── Additional integrity checks ─────────────────────────────────────────────── -test("exactly 11 skills have custom blocks", () => { +test("exactly 12 skills have custom blocks", () => { const withCustomBlocks: string[] = []; for (const id of ALL_IDS) { const skillPath = path.join(SKILLS_DIR, id, "SKILL.md"); @@ -128,7 +129,7 @@ test("exactly 11 skills have custom blocks", () => { assert.deepEqual( withCustomBlocks.sort(), expectedIds, - `Expected exactly these 11 custom-block IDs: ${expectedIds.join(", ")}\nActual: ${withCustomBlocks.join(", ")}`, + `Expected exactly these 12 custom-block IDs: ${expectedIds.join(", ")}\nActual: ${withCustomBlocks.join(", ")}`, ); }); diff --git a/tests/integration/agent-skills-discovery.test.ts b/tests/integration/agent-skills-discovery.test.ts index 9e80ff4d18..6cc9a8dc52 100644 --- a/tests/integration/agent-skills-discovery.test.ts +++ b/tests/integration/agent-skills-discovery.test.ts @@ -69,8 +69,8 @@ test("every CLI skill ID has skills//SKILL.md on disk", () => { assert.deepEqual(missing, [], `Missing CLI SKILL.md files: ${missing.join(", ")}`); }); -test("total skill count is exactly 43 (23 API + 20 CLI)", () => { - assert.equal(API_SKILL_IDS.length + CLI_SKILL_IDS.length, 43); +test("total skill count is exactly 44 (23 API + 21 CLI)", () => { + assert.equal(API_SKILL_IDS.length + CLI_SKILL_IDS.length, 44); }); // ── §2: Frontmatter validation ──────────────────────────────────────────────── @@ -120,11 +120,11 @@ test("each SKILL.md body is at least 100 chars", () => { // ── §3: MCP tool omniroute_agent_skills_list ───────────────────────────────── -test("MCP omniroute_agent_skills_list handler returns count 44 (43 + config)", async () => { +test("MCP omniroute_agent_skills_list handler returns count 45 (44 + config)", async () => { const result = await agentSkillTools.omniroute_agent_skills_list.handler({}); - assert.equal(result.count, 44, `Expected 44 but got ${result.count}`); + assert.equal(result.count, 45, `Expected 45 but got ${result.count}`); assert.ok(Array.isArray(result.skills)); - assert.equal(result.skills.length, 44); + assert.equal(result.skills.length, 45); }); test("MCP omniroute_agent_skills_list result has all 42 IDs", async () => { @@ -157,9 +157,9 @@ test("A2A list-capabilities artifact content contains 42 skill IDs as table rows assert.ok(rows.length >= 42, `Expected at least 42 data rows but got ${rows.length}`); }); -test("A2A list-capabilities metadata.totalSkills === 44 (43 + config)", async () => { +test("A2A list-capabilities metadata.totalSkills === 45 (44 + config)", async () => { const result = await executeListCapabilities(stubTask); - assert.equal(result.metadata.totalSkills, 44); + assert.equal(result.metadata.totalSkills, 45); }); test("A2A list-capabilities artifact contains all 42 skill IDs", async () => { diff --git a/tests/integration/codex-chat-reasoning-http-e2e.test.ts b/tests/integration/codex-chat-reasoning-http-e2e.test.ts new file mode 100644 index 0000000000..e5a03749ff --- /dev/null +++ b/tests/integration/codex-chat-reasoning-http-e2e.test.ts @@ -0,0 +1,303 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import fsp from "node:fs/promises"; +import http from "node:http"; +import os from "node:os"; +import path from "node:path"; +import { once } from "node:events"; + +const CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses"; +const ENCRYPTED_CONTENT_SENTINEL = "encrypted-codex-state:" + "A".repeat(910); +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-codex-chat-http-")); + +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.DISABLE_SQLITE_AUTO_BACKUP = "true"; +process.env.API_KEY_SECRET = "codex-chat-http-e2e-secret-123456"; +process.env.REQUIRE_API_KEY = "false"; +process.env.OMNIROUTE_LOG_REQUEST_SHAPE = "0"; + +const core = await import("../../src/lib/db/core.ts"); +const providersDb = await import("../../src/lib/db/providers.ts"); +const chatRoute = await import("../../src/app/api/v1/chat/completions/route.ts"); + +const originalFetch = globalThis.fetch; + +type RecordedRequest = { + url: string; + method: string; + body: Record; +}; + +function responsesEvents() { + const response = { + id: "resp_reasoning_http", + object: "response", + status: "in_progress", + model: "gpt-5.6-sol", + output: [], + }; + return [ + { type: "response.created", response }, + { + type: "response.output_item.added", + output_index: 0, + item: { + id: "rs_reasoning_http", + type: "reasoning", + encrypted_content: ENCRYPTED_CONTENT_SENTINEL, + summary: [], + }, + }, + { + type: "response.output_item.done", + output_index: 0, + item: { + id: "rs_reasoning_http", + type: "reasoning", + encrypted_content: ENCRYPTED_CONTENT_SENTINEL, + summary: [], + }, + }, + { + type: "response.output_item.added", + output_index: 1, + item: { id: "msg_reasoning_http", type: "message", role: "assistant", content: [] }, + }, + { + type: "response.content_part.added", + item_id: "msg_reasoning_http", + output_index: 1, + content_index: 0, + part: { type: "output_text", text: "", annotations: [] }, + }, + { + type: "response.output_text.delta", + item_id: "msg_reasoning_http", + output_index: 1, + content_index: 0, + delta: "The answer is 42.", + }, + { + type: "response.output_text.done", + item_id: "msg_reasoning_http", + output_index: 1, + content_index: 0, + text: "The answer is 42.", + }, + { + type: "response.output_item.done", + output_index: 1, + item: { + id: "msg_reasoning_http", + type: "message", + role: "assistant", + content: [{ type: "output_text", text: "The answer is 42.", annotations: [] }], + }, + }, + { + type: "response.completed", + response: { + ...response, + status: "completed", + output: [ + { + id: "rs_reasoning_http", + type: "reasoning", + summary: [{ type: "summary_text", text: "I checked the contract. " }], + }, + { + id: "msg_reasoning_http", + type: "message", + role: "assistant", + content: [{ type: "output_text", text: "The answer is 42.", annotations: [] }], + }, + ], + usage: { input_tokens: 8, output_tokens: 9, total_tokens: 17 }, + }, + }, + ]; +} + +function mockResponsesSse() { + const nativeFraming = process.env.CODEX_NATIVE_EVENT_FRAMING === "1"; + return responsesEvents() + .map((event) => { + const eventLine = nativeFraming ? `event: ${event.type}\n` : ""; + return `${eventLine}data: ${JSON.stringify(event)}\n\n`; + }) + .join(""); +} + +async function readIncomingBody(request: http.IncomingMessage) { + const chunks: Buffer[] = []; + for await (const chunk of request) + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + return Buffer.concat(chunks); +} + +async function bridgeRouteResponse(response: Response, outgoing: http.ServerResponse) { + outgoing.writeHead(response.status, Object.fromEntries(response.headers.entries())); + if (!response.body) { + outgoing.end(); + return; + } + + const reader = response.body.getReader(); + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + if (!outgoing.write(value)) await once(outgoing, "drain"); + } + outgoing.end(); + } finally { + reader.releaseLock(); + } +} + +async function startRouteServer() { + const server = http.createServer(async (incoming, outgoing) => { + try { + if (incoming.method !== "POST" || incoming.url !== "/v1/chat/completions") { + outgoing.writeHead(404).end(); + return; + } + + const body = await readIncomingBody(incoming); + const address = server.address(); + assert(address && typeof address !== "string"); + const headers = new Headers(); + for (const [name, value] of Object.entries(incoming.headers)) { + if (Array.isArray(value)) value.forEach((item) => headers.append(name, item)); + else if (value !== undefined) headers.set(name, value); + } + const request = new Request(`http://127.0.0.1:${address.port}${incoming.url}`, { + method: incoming.method, + headers, + body, + }); + await bridgeRouteResponse(await chatRoute.POST(request), outgoing); + } catch (error) { + // Mock route bridge: surface the message, never the raw stack (js/stack-trace-exposure). + outgoing.writeHead(500, { "content-type": "text/plain" }); + outgoing.end(error instanceof Error ? error.message : String(error)); + } + }); + + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + const address = server.address(); + assert(address && typeof address !== "string"); + return { server, url: `http://127.0.0.1:${address.port}/v1/chat/completions` }; +} + +function parseSse(raw: string) { + return raw + .split(/\n\n+/) + .map((block) => + block + .split("\n") + .find((line) => line.startsWith("data: ")) + ?.slice(6) + ) + .filter((data): data is string => Boolean(data)); +} + +async function closeServer(server: http.Server) { + if (!server.listening) return; + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())) + ); +} + +test("chat completions streams Codex Responses reasoning through real route HTTP", async () => { + const recorded: RecordedRequest[] = []; + let routeServer: http.Server | undefined; + + try { + await providersDb.createProviderConnection({ + provider: "codex", + authType: "oauth", + name: "codex-http-reasoning", + email: "codex-http@example.test", + accessToken: "mock-codex-access-token", + refreshToken: "mock-codex-refresh-token", + tokenType: "Bearer", + expiresAt: new Date(Date.now() + 60 * 60 * 1000).toISOString(), + isActive: true, + testStatus: "active", + providerSpecificData: {}, + }); + + const routeHarness = await startRouteServer(); + routeServer = routeHarness.server; + + globalThis.fetch = async (input, init) => { + const request = input instanceof Request ? input : new Request(input, init); + if (request.url !== CODEX_RESPONSES_URL) { + throw new Error(`Unexpected external fetch in Codex HTTP test: ${request.url}`); + } + recorded.push({ + url: request.url, + method: request.method, + body: JSON.parse(await request.text()) as Record, + }); + return new Response(mockResponsesSse(), { + status: 200, + headers: { "content-type": "text/event-stream; charset=utf-8" }, + }); + }; + + const response = await originalFetch(routeHarness.url, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + model: "codex/gpt-5.6-sol", + stream: true, + reasoning_effort: "high", + messages: [{ role: "user", content: "What is the answer?" }], + }), + }); + const raw = await response.text(); + + assert.equal(response.status, 200, raw); + assert.match(response.headers.get("content-type") ?? "", /^text\/event-stream/); + assert.equal(recorded.length, 1); + assert.equal(recorded[0].url, CODEX_RESPONSES_URL); + assert.equal(recorded[0].method, "POST"); + assert.deepEqual(recorded[0].body.reasoning, { effort: "high", summary: "auto" }); + assert.deepEqual(recorded[0].body.input, [ + { + type: "message", + role: "user", + content: [{ type: "input_text", text: "What is the answer?" }], + }, + ]); + + const chunks = parseSse(raw); + assert.equal(chunks.at(-1), "[DONE]"); + const payloads = chunks.slice(0, -1).map((chunk) => JSON.parse(chunk)); + const reasoningContentDeltas = payloads + .map((payload) => payload.choices?.[0]?.delta?.reasoning_content) + .filter((content): content is string => Boolean(content)); + assert.equal(reasoningContentDeltas.length, 1); + const reasoningContent = reasoningContentDeltas.join(""); + assert.match(reasoningContent, /encrypted (?:state|private reasoning)/i); + assert(!raw.includes(ENCRYPTED_CONTENT_SENTINEL), raw); + assert(!reasoningContent.includes(ENCRYPTED_CONTENT_SENTINEL), reasoningContent); + assert( + payloads.some((payload) => payload.choices?.[0]?.delta?.content === "The answer is 42.") + ); + assert(!raw.includes("response.reasoning_summary_text.delta"), raw); + assert(!raw.includes('"type":"error"'), raw); + assert(!raw.includes('"error"'), raw); + } finally { + globalThis.fetch = originalFetch; + if (routeServer) await closeServer(routeServer); + core.closeDbInstance({ checkpointMode: null }); + await fsp.rm(TEST_DATA_DIR, { recursive: true, force: true }); + } +}); diff --git a/tests/unit/agent-bridge-dns-toggle-method-7157.test.ts b/tests/unit/agent-bridge-dns-toggle-method-7157.test.ts new file mode 100644 index 0000000000..53a7c4311a --- /dev/null +++ b/tests/unit/agent-bridge-dns-toggle-method-7157.test.ts @@ -0,0 +1,24 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const clientPath = path.resolve( + __dirname, + "../../src/app/(dashboard)/dashboard/tools/agent-bridge/AgentBridgePageClient.tsx" +); +const source = readFileSync(clientPath, "utf8"); + +test("#7157: dns toggle fetch call uses method POST (route.ts only exports POST)", () => { + const dnsCallMatch = source.match( + /\/api\/tools\/agent-bridge\/agents\/\$\{agentId\}\/dns`,\s*\{\s*method:\s*"([A-Z]+)"/ + ); + assert.ok(dnsCallMatch, "expected to find the dns fetch call in AgentBridgePageClient.tsx"); + assert.equal( + dnsCallMatch?.[1], + "POST", + "dns fetch call must use method: 'POST' to match the route.ts export (issue #7157)" + ); +}); diff --git a/tests/unit/agentSkillTools-mcp.test.ts b/tests/unit/agentSkillTools-mcp.test.ts index a34acea595..e344f14928 100644 --- a/tests/unit/agentSkillTools-mcp.test.ts +++ b/tests/unit/agentSkillTools-mcp.test.ts @@ -58,11 +58,11 @@ test("each agentSkillTool has name, description, inputSchema, and handler", () = // ─── omniroute_agent_skills_list ──────────────────────────────────────────── -test("omniroute_agent_skills_list with no filters returns all 44 skills", async () => { +test("omniroute_agent_skills_list with no filters returns all 45 skills", async () => { const result = await agentSkillTools.omniroute_agent_skills_list.handler({}); - assert.equal(result.count, 44, `Expected 44 but got ${result.count}`); + assert.equal(result.count, 45, `Expected 45 but got ${result.count}`); assert.ok(Array.isArray(result.skills)); - assert.equal(result.skills.length, 44); + assert.equal(result.skills.length, 45); }); test("omniroute_agent_skills_list({category:'api'}) returns exactly 23 entries", async () => { @@ -71,9 +71,9 @@ test("omniroute_agent_skills_list({category:'api'}) returns exactly 23 entries", assert.ok(result.skills.every((s: { category: string }) => s.category === "api")); }); -test("omniroute_agent_skills_list({category:'cli'}) returns exactly 20 entries", async () => { +test("omniroute_agent_skills_list({category:'cli'}) returns exactly 21 entries", async () => { const result = await agentSkillTools.omniroute_agent_skills_list.handler({ category: "cli" }); - assert.equal(result.count, 20, `Expected 20 cli skills but got ${result.count}`); + assert.equal(result.count, 21, `Expected 21 cli skills but got ${result.count}`); assert.ok(result.skills.every((s: { category: string }) => s.category === "cli")); }); @@ -83,7 +83,7 @@ test("omniroute_agent_skills_list result includes coverage shape", async () => { assert.ok(typeof result.coverage.api === "object"); assert.ok(typeof result.coverage.cli === "object"); assert.equal(result.coverage.api.total, 23); - assert.equal(result.coverage.cli.total, 20); + assert.equal(result.coverage.cli.total, 21); assert.ok(typeof result.coverage.totalSkills === "number"); assert.ok(typeof result.coverage.generatedAt === "string"); }); @@ -168,11 +168,11 @@ test("omniroute_agent_skills_coverage({}) returns coverage shape", async () => { assert.ok(typeof result.api === "object"); assert.ok(typeof result.cli === "object"); assert.equal(result.api.total, 23); - assert.equal(result.cli.total, 20); + assert.equal(result.cli.total, 21); assert.ok(typeof result.api.have === "number"); assert.ok(typeof result.cli.have === "number"); assert.ok(result.api.have >= 0 && result.api.have <= 23); - assert.ok(result.cli.have >= 0 && result.cli.have <= 20); + assert.ok(result.cli.have >= 0 && result.cli.have <= 21); assert.ok(typeof result.totalSkills === "number"); assert.equal(result.totalSkills, result.api.have + result.cli.have + (result.config?.have ?? 0)); assert.ok(typeof result.generatedAt === "string"); diff --git a/tests/unit/agentSkills-catalog.test.ts b/tests/unit/agentSkills-catalog.test.ts index 9820c627c7..eec31d49ad 100644 --- a/tests/unit/agentSkills-catalog.test.ts +++ b/tests/unit/agentSkills-catalog.test.ts @@ -15,10 +15,10 @@ const agentSkillsConstants = await import("../../src/shared/constants/agentSkill // ─── Counts ─────────────────────────────────────────────────────────────────── -test("getCatalog() returns exactly 44 entries", () => { +test("getCatalog() returns exactly 45 entries", () => { refreshCatalog(); const catalog = getCatalog(); - assert.equal(catalog.length, 44, `Expected 44 but got ${catalog.length}`); + assert.equal(catalog.length, 45, `Expected 45 but got ${catalog.length}`); }); test("API_SKILL_IDS has exactly 23 entries", () => { @@ -26,7 +26,7 @@ test("API_SKILL_IDS has exactly 23 entries", () => { }); test("CLI_SKILL_IDS has exactly 20 entries", () => { - assert.equal(CLI_SKILL_IDS.length, 20); + assert.equal(CLI_SKILL_IDS.length, 21); }); test("getCatalog() contains exactly 22 api skills", () => { @@ -34,9 +34,9 @@ test("getCatalog() contains exactly 22 api skills", () => { assert.equal(apiSkills.length, 23); }); -test("getCatalog() contains exactly 20 cli skills", () => { +test("getCatalog() contains exactly 21 cli skills", () => { const cliSkills = getCatalog().filter((s) => s.category === "cli"); - assert.equal(cliSkills.length, 20); + assert.equal(cliSkills.length, 21); }); // ─── ID format ──────────────────────────────────────────────────────────────── @@ -160,9 +160,9 @@ test("filterCatalog({ category: 'api' }) returns 23 api skills", () => { } }); -test("filterCatalog({ category: 'cli' }) returns 20 cli skills", () => { +test("filterCatalog({ category: 'cli' }) returns 21 cli skills", () => { const skills = filterCatalog({ category: "cli" }); - assert.equal(skills.length, 20); + assert.equal(skills.length, 21); for (const s of skills) { assert.equal(s.category, "cli"); } @@ -185,9 +185,9 @@ test("filterCatalog({ area: 'nonexistent' }) returns empty array", () => { assert.equal(skills.length, 0); }); -test("filterCatalog({}) returns full catalog (44 entries)", () => { +test("filterCatalog({}) returns full catalog (45 entries)", () => { const skills = filterCatalog({}); - assert.equal(skills.length, 44); + assert.equal(skills.length, 45); }); // ─── refreshCatalog ─────────────────────────────────────────────────────────── @@ -214,9 +214,9 @@ test("computeCoverage() returns valid SkillCoverage shape", () => { assert.ok(cov.api.have >= 0 && cov.api.have <= 23); assert.ok(typeof cov.cli === "object"); - assert.equal(cov.cli.total, 20); + assert.equal(cov.cli.total, 21); assert.ok(typeof cov.cli.have === "number"); - assert.ok(cov.cli.have >= 0 && cov.cli.have <= 20); + assert.ok(cov.cli.have >= 0 && cov.cli.have <= 21); assert.equal(cov.totalSkills, cov.api.have + cov.cli.have + (cov.config?.have ?? 0)); @@ -255,6 +255,6 @@ test("CLI_SKILL_IDS first entry is cli-serve", () => { assert.equal(CLI_SKILL_IDS[0], "cli-serve"); }); -test("CLI_SKILL_IDS last entry is cli-setup", () => { - assert.equal(CLI_SKILL_IDS[CLI_SKILL_IDS.length - 1], "cli-setup"); +test("CLI_SKILL_IDS last entry is cli-skill-collector", () => { + assert.equal(CLI_SKILL_IDS[CLI_SKILL_IDS.length - 1], "cli-skill-collector"); }); diff --git a/tests/unit/agentSkills-generator.test.ts b/tests/unit/agentSkills-generator.test.ts index db54f73ad2..95475887d9 100644 --- a/tests/unit/agentSkills-generator.test.ts +++ b/tests/unit/agentSkills-generator.test.ts @@ -61,11 +61,11 @@ test("dry-run (default) returns report without writing any files", async () => { outputDir: tmpDir, }); - // All 44 skills should appear as generated (would-write) since dir is empty + // All 45 skills should appear as generated (would-write) since dir is empty assert.equal( report.generated.length + report.unchanged.length, - 44, - `Expected 44 total (generated+unchanged), got generated=${report.generated.length} unchanged=${report.unchanged.length}`, + 45, + `Expected 45 total (generated+unchanged), got generated=${report.generated.length} unchanged=${report.unchanged.length}`, ); assert.equal(report.errors.length, 0, `Unexpected errors: ${JSON.stringify(report.errors)}`); @@ -81,7 +81,7 @@ test("dry-run (default) returns report without writing any files", async () => { } }); -test("dry-run generates report with 44 total (generated+unchanged)", async () => { +test("dry-run generates report with 45 total (generated+unchanged)", async () => { const tmpDir = mkTmpDir(); try { refreshCatalog(); @@ -91,7 +91,7 @@ test("dry-run generates report with 44 total (generated+unchanged)", async () => outputDir: tmpDir, }); const total = report.generated.length + report.unchanged.length; - assert.equal(total, 44); + assert.equal(total, 45); } finally { rmTmpDir(tmpDir); } @@ -134,7 +134,7 @@ test("apply mode writes SKILL.md with valid frontmatter for omni-providers", asy } }); -test("apply mode writes all 44 SKILL.md files when no onlyIds filter", async () => { +test("apply mode writes all 45 SKILL.md files when no onlyIds filter", async () => { const tmpDir = mkTmpDir(); try { refreshCatalog(); @@ -145,7 +145,7 @@ test("apply mode writes all 44 SKILL.md files when no onlyIds filter", async () }); assert.equal(report.errors.length, 0, `Errors: ${JSON.stringify(report.errors)}`); - assert.equal(report.generated.length, 44); + assert.equal(report.generated.length, 45); // Verify all dirs exist const catalog = getCatalog(); diff --git a/tests/unit/agentSkills-routes.test.ts b/tests/unit/agentSkills-routes.test.ts index 16265f4a4f..1de6eccf2d 100644 --- a/tests/unit/agentSkills-routes.test.ts +++ b/tests/unit/agentSkills-routes.test.ts @@ -101,15 +101,15 @@ test.after(() => { // GET /api/agent-skills // ═════════════════════════════════════════════════════════════════════════════ -test("GET /api/agent-skills — returns 44 skills with count and coverage", async () => { +test("GET /api/agent-skills — returns 45 skills with count and coverage", async () => { const req = makeRequest("GET", "http://localhost/api/agent-skills"); const res = await listRoute.GET(req); assert.equal(res.status, 200); const body = (await res.json()) as { skills: unknown[]; count: number; coverage: unknown }; - assert.equal(body.count, 44, `Expected 44 skills but got ${body.count}`); + assert.equal(body.count, 45, `Expected 45 skills but got ${body.count}`); assert.equal(Array.isArray(body.skills), true); - assert.equal(body.skills.length, 44); + assert.equal(body.skills.length, 45); assert.ok(body.coverage !== undefined, "coverage should be present"); }); @@ -123,13 +123,13 @@ test("GET /api/agent-skills?category=api — returns 23 api skills", async () => assert.ok(body.skills.every((s) => s.category === "api"), "All skills should be api category"); }); -test("GET /api/agent-skills?category=cli — returns 20 cli skills", async () => { +test("GET /api/agent-skills?category=cli — returns 21 cli skills", async () => { const req = makeRequest("GET", "http://localhost/api/agent-skills?category=cli"); const res = await listRoute.GET(req); assert.equal(res.status, 200); const body = (await res.json()) as { skills: Array<{ category: string }>; count: number }; - assert.equal(body.count, 20); + assert.equal(body.count, 21); assert.ok(body.skills.every((s) => s.category === "cli"), "All skills should be cli category"); }); @@ -268,7 +268,7 @@ test("GET /api/agent-skills/coverage — returns valid SkillCoverage shape", asy }; assert.equal(body.api.total, 23, "api.total must be 23"); - assert.equal(body.cli.total, 20, "cli.total must be 20"); + assert.equal(body.cli.total, 21, "cli.total must be 21"); assert.ok(typeof body.totalSkills === "number", "totalSkills must be a number"); assert.ok(typeof body.generatedAt === "string", "generatedAt must be a string"); // generatedAt must be a valid ISO datetime diff --git a/tests/unit/antigravity-streaming-error-body-sanitized-2461.test.ts b/tests/unit/antigravity-streaming-error-body-sanitized-2461.test.ts new file mode 100644 index 0000000000..41d242c1f5 --- /dev/null +++ b/tests/unit/antigravity-streaming-error-body-sanitized-2461.test.ts @@ -0,0 +1,63 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; + +import { AntigravityExecutor } from "../../open-sse/executors/antigravity.ts"; +import { + clearAntigravityVersionCache, + seedAntigravityVersionCache, +} from "../../open-sse/services/antigravityVersion.ts"; + +// Ports decolua/9router#2461: a non-ok (e.g. 403) Antigravity upstream response in the +// STREAMING path was piped straight through to the client via a raw pass-through +// TransformStream, with no `response.ok` check at all — unlike the non-streaming path, +// which already builds a sanitized error via buildAntigravityUpstreamError. When the +// upstream 403 body is gzip-compressed (or otherwise binary/non-UTF8), those raw bytes +// end up surfaced verbatim in the client-visible error message, corrupting it (reporters +// saw literal control-byte garbage after "[ERROR] [403]:"). +test.afterEach(() => { + clearAntigravityVersionCache(); +}); + +test("AntigravityExecutor.execute (stream=true) sanitizes a non-ok upstream body instead of piping raw bytes", async () => { + const executor = new AntigravityExecutor(); + const originalFetch = globalThis.fetch; + seedAntigravityVersionCache("2026.04.17-test"); + + // Simulate a gzip-compressed 403 body (magic bytes 0x1f 0x8b), the exact shape + // reported upstream — reading it as text without decoding produces garbage. + const binaryBody = new Uint8Array([0x1f, 0x8b, 0x08, 0x00, 0x02, 0xff, 0x52, 0x41, 0x4e]); + + globalThis.fetch = async () => + new Response(binaryBody, { + status: 403, + headers: { "Content-Type": "application/json" }, + }); + + try { + const result = await executor.execute({ + model: "antigravity/gemini-2.5-flash", + body: { request: { contents: [] } }, + stream: true, + credentials: { accessToken: "token", projectId: "project-1" }, + log: { debug() {}, warn() {} }, + }); + + assert.equal(result.response.status, 403); + + const bodyText = await result.response.text(); + + // The raw gzip magic bytes must never reach the client-visible error text. + assert.ok( + !bodyText.includes("\x1f\x8b"), + `expected sanitized error body, got raw bytes leaking through: ${JSON.stringify(bodyText)}` + ); + + // Must be routed through buildErrorBody()/buildAntigravityUpstreamError() — a clean, + // parseable JSON error shape (hard rule #12), not an arbitrary pass-through stream. + const parsed = JSON.parse(bodyText) as { error?: { message?: string } }; + assert.ok(parsed.error?.message, "expected a structured error.message"); + assert.match(parsed.error.message, /Antigravity upstream error \(403\)/); + } finally { + globalThis.fetch = originalFetch; + } +}); diff --git a/tests/unit/api/services/cliproxy-provider-expose.test.ts b/tests/unit/api/services/cliproxy-provider-expose.test.ts new file mode 100644 index 0000000000..3376c532df --- /dev/null +++ b/tests/unit/api/services/cliproxy-provider-expose.test.ts @@ -0,0 +1,95 @@ +/** + * Tests for POST /api/services/cliproxy/provider-expose + */ + +import { beforeEach, describe, it } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-cliproxy-provider-expose-")); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.NODE_ENV = "test"; +process.env.DISABLE_SQLITE_AUTO_BACKUP = "true"; + +// Initialise DB first. +const core = await import("../../../../src/lib/db/core.ts"); +const { upsertVersionManagerTool, getVersionManagerTool } = + await import("../../../../src/lib/db/versionManager.ts"); + +const { POST } = await import("../../../../src/app/api/services/cliproxy/provider-expose/route.ts"); + +function resetDb() { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); +} + +function makeRequest(body: unknown): Request { + return new Request("http://localhost/api/services/cliproxy/provider-expose", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); +} + +beforeEach(() => { + resetDb(); +}); + +describe("POST /api/services/cliproxy/provider-expose", () => { + it("returns 400 for invalid JSON body", async () => { + const req = new Request("http://localhost/api/services/cliproxy/provider-expose", { + method: "POST", + body: "not-json", + headers: { "Content-Type": "application/json" }, + }); + const res = await POST(req); + assert.equal(res.status, 400); + }); + + it("returns 400 when enabled is missing", async () => { + const req = makeRequest({}); + const res = await POST(req); + assert.equal(res.status, 400); + const body = await res.json(); + assert.ok(body?.error?.message ?? body?.message, "should have an error message"); + }); + + it("returns 400 when enabled is not a boolean", async () => { + const req = makeRequest({ enabled: "yes" }); + const res = await POST(req); + assert.equal(res.status, 400); + }); + + it("sets providerExpose to true and returns 204", async () => { + await upsertVersionManagerTool({ tool: "cliproxy", status: "stopped" }); + + const req = makeRequest({ enabled: true }); + const res = await POST(req); + assert.equal(res.status, 204); + + const row = await getVersionManagerTool("cliproxy"); + assert.equal(row?.providerExpose, true, "providerExpose should be true in DB"); + }); + + it("sets providerExpose to false and returns 204", async () => { + await upsertVersionManagerTool({ tool: "cliproxy", status: "stopped" }); + + await POST(makeRequest({ enabled: true })); + const res = await POST(makeRequest({ enabled: false })); + assert.equal(res.status, 204); + + const row = await getVersionManagerTool("cliproxy"); + assert.equal(row?.providerExpose, false, "providerExpose should be false in DB"); + }); + + it("error response does not leak stack traces", async () => { + const req = makeRequest({}); + const res = await POST(req); + assert.equal(res.status, 400); + const text = await res.text(); + assert.ok(!text.includes("at /"), "error body must not expose stack trace"); + }); +}); diff --git a/tests/unit/api/v1/bifrost-sidecar.test.ts b/tests/unit/api/v1/bifrost-sidecar.test.ts index 286e8bffcb..8e9f5a3c8f 100644 --- a/tests/unit/api/v1/bifrost-sidecar.test.ts +++ b/tests/unit/api/v1/bifrost-sidecar.test.ts @@ -79,9 +79,8 @@ test("bifrost route: returns 503 + fallback header when BIFROST_BASE_URL is unse delete process.env.BIFROST_STREAMING_ENABLED; // Dynamic import after env is set so the module reads the empty value. - const { POST } = await import( - "../../../../src/app/api/v1/relay/chat/completions/bifrost/route.ts" - ); + const { POST } = + await import("../../../../src/app/api/v1/relay/chat/completions/bifrost/route.ts"); const req = new Request("http://localhost/api/v1/relay/chat/completions/bifrost", { method: "POST", @@ -191,12 +190,14 @@ test("bifrost route: records relay usage after SSE stream completion", async () delete process.env.BIFROST_STREAMING_ENABLED; const relayToken = seedRelayToken(`relay_bifrost_sse_${Date.now()}`); + let forwardedRequestId: string | null = null; - globalThis.fetch = async () => - new Response( + globalThis.fetch = async (_input, init) => { + forwardedRequestId = new Headers(init?.headers).get("x-request-id"); + return new Response( new ReadableStream({ start(controller) { - controller.enqueue(new TextEncoder().encode("data: {\"delta\":\"hi\"}\n\n")); + controller.enqueue(new TextEncoder().encode('data: {"delta":"hi"}\n\n')); controller.close(); }, }), @@ -205,6 +206,7 @@ test("bifrost route: records relay usage after SSE stream completion", async () headers: { "content-type": "text/event-stream" }, } ); + }; const { POST } = await import( `../../../../src/app/api/v1/relay/chat/completions/bifrost/route.ts?case=${Date.now()}-${Math.random()}` @@ -227,6 +229,7 @@ test("bifrost route: records relay usage after SSE stream completion", async () const res = await POST(req); assert.equal(res.status, 200); assert.equal(res.headers.get("X-Routed-By"), "bifrost"); + assert.equal(forwardedRequestId, "bifrost-sse-lifecycle-test"); assert.equal(getRelayLogs(relayToken.id, 10).length, 0); assert.match(await res.text(), /delta/); diff --git a/tests/unit/api/v1/provider-plugin-manifest-route.test.ts b/tests/unit/api/v1/provider-plugin-manifest-route.test.ts index 9af7baa46d..fa59013021 100644 --- a/tests/unit/api/v1/provider-plugin-manifest-route.test.ts +++ b/tests/unit/api/v1/provider-plugin-manifest-route.test.ts @@ -4,18 +4,70 @@ import test from "node:test"; import { GET, OPTIONS, + injectServiceModelsIntoManifest, } from "../../../../src/app/api/v1/provider-plugin-manifest/route.ts"; +import type { ServiceModel } from "../../../../src/lib/db/serviceModels.ts"; +import type { ProviderPluginManifest, ProviderPluginManifestEntry } from "../../../../open-sse/config/providerPluginManifest.ts"; +import { generateProviderPluginManifest } from "../../../../open-sse/config/providerPluginManifestRegistry.ts"; + +function getProvider(manifest: ProviderPluginManifest, id: string): ProviderPluginManifestEntry | undefined { + return manifest.providers.find((provider) => provider.id === id); +} + +function hasModel(provider: ProviderPluginManifestEntry | undefined, modelId: string): boolean { + if (!provider) return false; + return provider.models.some((model) => model.id === modelId); +} + +function withServicePluginEntries(manifest: ProviderPluginManifest): ProviderPluginManifest { + const providers = [...manifest.providers]; + + if (!providers.some((provider) => provider.id === "9router")) { + providers.push({ + id: "9router", + format: "openai", + executor: "default", + auth: { type: "none", header: "authorization" }, + endpoints: {}, + capabilities: [], + passthroughModels: false, + models: [], + sidecar: { eligible: false, reasons: [] }, + }); + } + + if (!providers.some((provider) => provider.id === "cliproxyapi")) { + providers.push({ + id: "cliproxyapi", + format: "openai", + executor: "default", + auth: { type: "none", header: "authorization" }, + endpoints: {}, + capabilities: [], + passthroughModels: false, + models: [], + sidecar: { eligible: false, reasons: [] }, + }); + } + + providers.sort((a, b) => a.id.localeCompare(b.id)); + + return { + ...manifest, + providers, + }; +} test("provider plugin manifest route returns JSON-safe manifest", async () => { const response = await GET(); - const body = await response.json(); + const body = (await response.json()) as ProviderPluginManifest; assert.equal(response.status, 200); assert.equal(response.headers.get("Content-Type"), "application/json"); assert.equal(body.schemaVersion, 1); assert.equal(body.generatedFrom, "open-sse/config/providers"); assert.ok(body.providers.length > 100); - assert.ok(body.providers.some((provider: { id: string }) => provider.id === "openai")); + assert.ok(body.providers.some((provider) => provider.id === "openai")); const serialized = JSON.stringify(body); assert.equal(serialized.includes("clientSecret"), false); @@ -28,3 +80,136 @@ test("provider plugin manifest route handles CORS preflight", async () => { assert.equal(response.headers.get("Access-Control-Allow-Methods"), "GET, OPTIONS"); assert.equal(response.headers.get("Access-Control-Allow-Headers"), "*"); }); + +test("provider plugin manifest route injects service models with a custom reader", async () => { + const manifest = withServicePluginEntries(generateProviderPluginManifest()); + const withModels = await injectServiceModelsIntoManifest( + manifest, + (toolName: string): ServiceModel[] => { + if (toolName === "9router") { + return [ + { id: "gpt-test", name: "9Router Test", available: true }, + { id: "9router/chat", name: "Already namespaced", available: true }, + ]; + } + if (toolName === "cliproxyapi") { + return [{ id: "model-clone", name: "Cliproxy Test", available: true }]; + } + return []; + }, + ); + + const nineRouterEntry = getProvider(withModels, "9router"); + assert.ok(nineRouterEntry); + assert.ok(hasModel(nineRouterEntry, "9router/gpt-test")); + assert.ok(hasModel(nineRouterEntry, "9router/chat")); + + const cliproxyEntry = getProvider(withModels, "cliproxyapi"); + assert.ok(cliproxyEntry); + assert.ok(hasModel(cliproxyEntry, "cliproxyapi/model-clone")); +}); + +test("provider plugin manifest route injects providers absent from upstream registry", async () => { + const manifest = generateProviderPluginManifest(); + const withModels = await injectServiceModelsIntoManifest( + manifest, + (toolName: string): ServiceModel[] => { + if (toolName === "9router") { + return [{ id: "injected-model", name: "Runtime Model", available: true }]; + } + if (toolName === "cliproxyapi") { + return [{ id: "proxy-model", name: "Proxy Model", available: true }]; + } + return []; + } + ); + + const nineRouterEntry = getProvider(withModels, "9router"); + assert.ok(nineRouterEntry); + assert.ok(hasModel(nineRouterEntry, "9router/injected-model")); + assert.equal(nineRouterEntry.passthroughModels, true); + assert.equal(nineRouterEntry.endpoints?.modelsUrl, "/v1/models"); + assert.equal(nineRouterEntry.format, "openai"); + + const cliproxyEntry = getProvider(withModels, "cliproxyapi"); + assert.ok(cliproxyEntry); + assert.ok(hasModel(cliproxyEntry, "cliproxyapi/proxy-model")); + assert.equal(cliproxyEntry.passthroughModels, true); + assert.equal(cliproxyEntry.endpoints?.modelsUrl, "/v1/models"); + assert.equal(cliproxyEntry.format, "openai"); +}); + +test("provider plugin manifest route skips unavailable service models", async () => { + const manifest = withServicePluginEntries(generateProviderPluginManifest()); + const withModels = await injectServiceModelsIntoManifest( + manifest, + (toolName: string): ServiceModel[] => { + if (toolName === "9router") { + return [ + { id: "visible", name: "9Router Visible", available: true }, + { id: "hidden", name: "9Router Hidden", available: false }, + ]; + } + return []; + }, + ); + + const nineRouterEntry = getProvider(withModels, "9router"); + assert.ok(nineRouterEntry); + assert.ok(hasModel(nineRouterEntry, "9router/visible")); + assert.equal(hasModel(nineRouterEntry, "9router/hidden"), false); +}); + +test("provider plugin manifest route injects only when 9router exposure is enabled", async () => { + const manifest = withServicePluginEntries(generateProviderPluginManifest()); + const withModels = await injectServiceModelsIntoManifest( + manifest, + (toolName: string): ServiceModel[] => { + if (toolName === "9router") { + return [{ id: "gpt-test", name: "9Router Test" }]; + } + return []; + }, + (toolName: string): boolean => (toolName === "9router" ? false : true), + ); + + const nineRouterEntry = getProvider(withModels, "9router"); + assert.ok(nineRouterEntry); + assert.equal(hasModel(nineRouterEntry, "9router/gpt-test"), false); +}); + +test("provider plugin manifest route injects for cliproxy when exposure is enabled", async () => { + const manifest = withServicePluginEntries(generateProviderPluginManifest()); + const withModels = await injectServiceModelsIntoManifest( + manifest, + (toolName: string): ServiceModel[] => { + if (toolName === "cliproxyapi") { + return [{ id: "model-clone", name: "Cliproxy Test" }]; + } + return []; + }, + () => true, + ); + + const cliproxyEntry = getProvider(withModels, "cliproxyapi"); + assert.ok(cliproxyEntry); + assert.ok(hasModel(cliproxyEntry, "cliproxyapi/model-clone")); +}); + +test("provider plugin manifest route skips cliproxy models when exposure is disabled", async () => { + const manifest = withServicePluginEntries(generateProviderPluginManifest()); + const withModels = await injectServiceModelsIntoManifest( + manifest, + (toolName: string): ServiceModel[] => { + if (toolName === "cliproxyapi") { + return [{ id: "model-clone", name: "Cliproxy Test" }]; + } + return []; + }, + (toolName: string): boolean => (toolName === "cliproxyapi" ? false : true), + ); + + const cliproxyEntry = getProvider(withModels, "cliproxyapi"); + assert.ok(cliproxyEntry); + assert.equal(hasModel(cliproxyEntry, "cliproxyapi/model-clone"), false); +}); diff --git a/tests/unit/api/v1/relay-routing-backend.test.ts b/tests/unit/api/v1/relay-routing-backend.test.ts index 6e8e0e0aad..64cca27097 100644 --- a/tests/unit/api/v1/relay-routing-backend.test.ts +++ b/tests/unit/api/v1/relay-routing-backend.test.ts @@ -1,5 +1,6 @@ import { test } from "node:test"; import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; import { getBifrostRoutingConfig, getRoutingFallbackHeader, @@ -152,3 +153,26 @@ test("relay routing backend strict bifrost bypasses manifest eligibility", () => { tryBifrost: true } ); }); + +test("automatic relay keeps the Bifrost timeout active until an SSE stream finalizes", () => { + const routeSource = readFileSync( + new URL("../../../../src/app/api/v1/relay/chat/completions/route.ts", import.meta.url), + "utf8" + ); + const forwardToBifrost = routeSource.slice( + routeSource.indexOf("async function forwardToBifrost"), + routeSource.indexOf("export async function OPTIONS") + ); + const streamBranch = forwardToBifrost.slice( + forwardToBifrost.indexOf("if (wantsStream && upstream.body)"), + forwardToBifrost.indexOf("clearTimeout(tid);\n recordUsage(") + ); + + assert.match( + streamBranch, + /finalizeReadableStream\(upstream\.body, \(error\) => \{\s*clearTimeout\(tid\)/ + ); + assert.match(streamBranch, /const statusCode = timedOut \? 504 : upstream\.status/); + assert.match(streamBranch, /error && backend === "auto"/); + assert.match(streamBranch, /recordBifrostFailure\(/); +}); diff --git a/tests/unit/balance-e2e-shards.test.ts b/tests/unit/balance-e2e-shards.test.ts new file mode 100644 index 0000000000..6484dfccb2 --- /dev/null +++ b/tests/unit/balance-e2e-shards.test.ts @@ -0,0 +1,75 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { lptAssign, weightItems } from "../../scripts/quality/balance-e2e-shards.mjs"; + +// WS4.1 (v3.8.49 quality plan) — the E2E matrix skew was 14× (24m47s vs 1m47s) +// because Playwright --shard distributes by count, not duration. These tests pin +// the LPT packing invariants; the hard one is COMPLETENESS (a lost spec would +// silently hollow the suite — the CLI self-checks it and falls back to --shard). + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); + +test("lptAssign puts the heaviest item alone before doubling up lighter shards", () => { + const shards = lptAssign( + [ + { file: "huge.spec.ts", weight: 100 }, + { file: "a.spec.ts", weight: 30 }, + { file: "b.spec.ts", weight: 30 }, + { file: "c.spec.ts", weight: 30 }, + ], + 2 + ); + assert.deepEqual(shards[0].files, ["huge.spec.ts"]); + assert.deepEqual(shards[1].files, ["a.spec.ts", "b.spec.ts", "c.spec.ts"]); + assert.equal(shards[0].total, 100); + assert.equal(shards[1].total, 90); +}); + +test("lptAssign is deterministic on equal weights (filename tiebreak)", () => { + const items = [ + { file: "b.spec.ts", weight: 10 }, + { file: "a.spec.ts", weight: 10 }, + ]; + const s1 = lptAssign(items, 2); + const s2 = lptAssign([...items].reverse(), 2); + assert.deepEqual(s1, s2); +}); + +test("completeness: every file lands in exactly one shard", () => { + const items = Array.from({ length: 37 }, (_, i) => ({ + file: `f${String(i).padStart(2, "0")}.spec.ts`, + weight: (i * 7) % 40, + })); + const shards = lptAssign(items, 9); + const union = shards.flatMap((s) => s.files).sort(); + assert.deepEqual(union, items.map((i) => i.file).sort()); +}); + +test("weightItems gives unknown/new specs the median weight, not an extreme", () => { + const items = weightItems(["new.spec.ts", "big.spec.ts", "small.spec.ts"], { + _meta: "x", + "big.spec.ts": 600, + "small.spec.ts": 20, + "other.spec.ts": 100, + }); + const byFile = Object.fromEntries(items.map((i) => [i.file, i.weight])); + assert.equal(byFile["big.spec.ts"], 600); + assert.equal(byFile["small.spec.ts"], 20); + assert.equal(byFile["new.spec.ts"], 100); // median of [20,100,600] +}); + +test("the committed timings seed covers every current e2e spec (no drift)", () => { + const timings = JSON.parse( + fs.readFileSync(path.join(ROOT, "config", "quality", "e2e-timings.json"), "utf8") + ); + const specs = fs + .readdirSync(path.join(ROOT, "tests", "e2e")) + .filter((f) => f.endsWith(".spec.ts")); + const missing = specs.filter((f) => !(f in timings)); + // Missing entries are tolerated at runtime (median fallback) — this assert keeps + // the seed honest so balance quality does not silently rot as specs are added. + assert.deepEqual(missing, [], `add to config/quality/e2e-timings.json: ${missing.join(", ")}`); +}); diff --git a/tests/unit/build/check-dashboard-typecheck.test.ts b/tests/unit/build/check-dashboard-typecheck.test.ts new file mode 100644 index 0000000000..dec6d54fad --- /dev/null +++ b/tests/unit/build/check-dashboard-typecheck.test.ts @@ -0,0 +1,111 @@ +// tests/unit/build/check-dashboard-typecheck.test.ts +// Unit tests for the pure parsing/diff helpers in check-dashboard-typecheck.mjs. +// No child process is spawned — synthetic tsc-style output only, so the suite is +// fast and hermetic. Proves the gate actually DETECTS the #6625/#6909 bug class +// (an orphaned identifier — used but not declared — in a dashboard TSX file), +// not just that the script runs. + +import test from "node:test"; +import assert from "node:assert/strict"; +import { + parseTscOutput, + diffAgainstBaseline, +} from "../../../scripts/check/check-dashboard-typecheck.mjs"; + +test("parseTscOutput: parses a TS2304 orphaned-identifier error (the #6625/#6909 bug class)", () => { + const raw = + `src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx(564,7): error TS2304: Cannot find name 'setPoolLoaded'.\n` + + `src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx(1204,12): error TS2304: Cannot find name 'poolLoaded'.\n`; + + const counts = parseTscOutput(raw); + + assert.deepEqual(counts, { + "src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx": { + TS2304: 2, + }, + }); +}); + +test("parseTscOutput: ignores non-error lines (summary/info output)", () => { + const raw = + `Some info line that is not an error\n` + + `src/app/(dashboard)/dashboard/foo.tsx(1,1): error TS2339: Property 'bar' does not exist.\n` + + `Found 1 error in 1 file.\n`; + + const counts = parseTscOutput(raw); + + assert.deepEqual(counts, { + "src/app/(dashboard)/dashboard/foo.tsx": { TS2339: 1 }, + }); +}); + +test("parseTscOutput: returns empty map for clean output", () => { + assert.deepEqual(parseTscOutput(""), {}); + assert.deepEqual(parseTscOutput("Found 0 errors.\n"), {}); +}); + +test("diffAgainstBaseline: flags a brand-new orphaned-identifier error as a regression", () => { + const baseline = {}; + const live = { + "src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx": { + TS2304: 5, + }, + }; + + const { regressions, improvements } = diffAgainstBaseline(live, baseline); + + assert.equal(regressions.length, 1); + assert.equal( + regressions[0].file, + "src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx" + ); + assert.equal(regressions[0].code, "TS2304"); + assert.equal(regressions[0].liveCount, 5); + assert.equal(regressions[0].baselineCount, 0); + assert.equal(improvements.length, 0); +}); + +test("diffAgainstBaseline: does NOT flag a frozen pre-existing error within its baselined count", () => { + const baseline = { "src/app/(dashboard)/dashboard/foo.tsx": { TS2339: 3 } }; + const live = { "src/app/(dashboard)/dashboard/foo.tsx": { TS2339: 3 } }; + + const { regressions, improvements } = diffAgainstBaseline(live, baseline); + + assert.equal(regressions.length, 0); + assert.equal(improvements.length, 0); +}); + +test("diffAgainstBaseline: flags a count INCREASE beyond the frozen baseline as a regression", () => { + const baseline = { "src/app/(dashboard)/dashboard/foo.tsx": { TS2339: 2 } }; + const live = { "src/app/(dashboard)/dashboard/foo.tsx": { TS2339: 3 } }; + + const { regressions } = diffAgainstBaseline(live, baseline); + + assert.equal(regressions.length, 1); + assert.equal(regressions[0].baselineCount, 2); + assert.equal(regressions[0].liveCount, 3); +}); + +test("diffAgainstBaseline: reports (does not fail on) a count DECREASE as an improvement", () => { + const baseline = { "src/app/(dashboard)/dashboard/foo.tsx": { TS2339: 3 } }; + const live = { "src/app/(dashboard)/dashboard/foo.tsx": { TS2339: 1 } }; + + const { regressions, improvements } = diffAgainstBaseline(live, baseline); + + assert.equal(regressions.length, 0); + assert.equal(improvements.length, 1); + assert.equal(improvements[0].baselineCount, 3); + assert.equal(improvements[0].liveCount, 1); +}); + +test("diffAgainstBaseline: a baselined error that fully disappears is reported as an improvement, not a failure", () => { + const baseline = { "src/app/(dashboard)/dashboard/foo.tsx": { TS2339: 2 } }; + const live = {}; + + const { regressions, improvements } = diffAgainstBaseline(live, baseline); + + assert.equal(regressions.length, 0); + assert.equal(improvements.length, 1); + assert.equal(improvements[0].liveCount, 0); + assert.equal(improvements[0].baselineCount, 2); +}); diff --git a/tests/unit/chat-combo-live-test.test.ts b/tests/unit/chat-combo-live-test.test.ts index a734b02bc4..79f8a35a32 100644 --- a/tests/unit/chat-combo-live-test.test.ts +++ b/tests/unit/chat-combo-live-test.test.ts @@ -274,7 +274,10 @@ test("chat completions route emits early keepalive while waiting for stream read assert.match(response.headers.get("content-type") || "", /text\/event-stream/); const body = await readAll(response); - assert.match(body, /: omniroute-keepalive/); + assert.match( + body, + /data: \{"id":"omniroute-keepalive","object":"chat\.completion\.chunk"/ + ); assert.match(body, /OK/); assert.match(body, /\[DONE\]/); }); diff --git a/tests/unit/check-pack-boot.test.ts b/tests/unit/check-pack-boot.test.ts new file mode 100644 index 0000000000..b22ca557b5 --- /dev/null +++ b/tests/unit/check-pack-boot.test.ts @@ -0,0 +1,57 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { pickTarball, evaluateBoot, pickPort } from "../../scripts/check/check-pack-boot.mjs"; + +// WS1.2 (T1, v3.8.49 quality plan) — pure-function guards for the tarball boot-smoke +// gate that kills the #7065 class (published artifact crashes on every boot because a +// packaging list drifted; 3rd recurrence). The end-to-end path runs in CI's +// package-artifact job; these tests pin the decision logic. + +const SCRIPT_PATH = path.join( + path.dirname(fileURLToPath(import.meta.url)), + "../../scripts/check/check-pack-boot.mjs" +); + +test("pickTarball extracts the filename from npm pack --json output", () => { + assert.equal(pickTarball('[{"filename":"omniroute-3.8.49.tgz","size":1}]'), "omniroute-3.8.49.tgz"); +}); + +test("pickTarball normalizes scoped slashes to the on-disk dash form", () => { + assert.equal(pickTarball('[{"filename":"@scope/pkg-1.0.0.tgz"}]'), "@scope-pkg-1.0.0.tgz"); +}); + +test("pickTarball throws on empty/odd npm output instead of booting garbage", () => { + assert.throws(() => pickTarball("[]")); + assert.throws(() => pickTarball("{}")); +}); + +test("evaluateBoot passes on HTTP 200 + matching version, whatever the health status", () => { + const r = evaluateBoot(200, { version: "3.8.49", status: "warning" }, "3.8.49"); + assert.equal(r.ok, true); + assert.deepEqual(r.failures, []); +}); + +test("evaluateBoot fails on non-200, non-JSON body, and version mismatch", () => { + assert.equal(evaluateBoot(503, { version: "3.8.49" }, "3.8.49").ok, false); + assert.equal(evaluateBoot(200, null, "3.8.49").ok, false); + const wrong = evaluateBoot(200, { version: "3.8.48" }, "3.8.49"); + assert.equal(wrong.ok, false); + assert.match(wrong.failures[0], /3\.8\.48/); +}); + +test("pickPort stays inside the reserved smoke range for any pid", () => { + for (const seed of [0, 1, 4000, 65535, 123456]) { + const p = pickPort(seed); + assert.ok(p >= 23000 && p < 27000, `port ${p} out of range for seed ${seed}`); + } +}); + +test("source guard: the gate polls the real health endpoint of the INSTALLED binary", () => { + const src = readFileSync(SCRIPT_PATH, "utf8"); + assert.ok(src.includes('"install", "-g", "--prefix"'), "must install the packed tarball into a clean prefix"); + assert.ok(src.includes("/api/monitoring/health"), "must poll the health endpoint"); + assert.ok(src.indexOf("npm") < src.indexOf("spawn"), "pack+install must precede the boot spawn"); +}); diff --git a/tests/unit/classify-pr-changes.test.ts b/tests/unit/classify-pr-changes.test.ts index a2815786cc..4f4e30154d 100644 --- a/tests/unit/classify-pr-changes.test.ts +++ b/tests/unit/classify-pr-changes.test.ts @@ -15,7 +15,7 @@ import { classifyPaths } from "../../scripts/quality/classify-pr-changes.mjs"; test("pure docs PR → docs only (no code unit/lint bag)", () => { const c = classifyPaths(["docs/architecture/QUALITY_GATES.md", "README.md"]); - assert.deepEqual(c, { code: false, docs: true, i18n: false, workflow: false }); + assert.deepEqual(c, { code: false, docs: true, i18n: false, workflow: false, testsOnly: false }); }); test("openapi under docs/ → docs (contract gates live in docs-sync, not unit)", () => { @@ -26,7 +26,7 @@ test("openapi under docs/ → docs (contract gates live in docs-sync, not unit)" test("pure message catalog → i18n only (not full unit suite)", () => { const c = classifyPaths(["src/i18n/messages/en.json", "src/i18n/messages/ko.json"]); - assert.deepEqual(c, { code: false, docs: false, i18n: true, workflow: false }); + assert.deepEqual(c, { code: false, docs: false, i18n: true, workflow: false, testsOnly: false }); }); test("i18n tooling/scripts → i18n + code (tooling can break runtime paths)", () => { @@ -49,7 +49,7 @@ test("workflow change → workflow + code (gates protect the gates)", () => { test("production source → code", () => { const c = classifyPaths(["open-sse/handlers/chatCore.ts", "src/lib/db/core.ts"]); - assert.deepEqual(c, { code: true, docs: false, i18n: false, workflow: false }); + assert.deepEqual(c, { code: true, docs: false, i18n: false, workflow: false, testsOnly: false }); }); test("mixed docs + code → both flags (jobs union their filters)", () => { @@ -65,5 +65,31 @@ test("unknown path → code fail-safe (never skip heavy gates by accident)", () test("empty change list → all false (nothing to validate)", () => { const c = classifyPaths([]); - assert.deepEqual(c, { code: false, docs: false, i18n: false, workflow: false }); + assert.deepEqual(c, { code: false, docs: false, i18n: false, workflow: false, testsOnly: false }); +}); + +// WS3.1 (v3.8.49 quality plan) — testsOnly powers the hotfix/test-only fast lane: +// a diff touching ONLY tests/ (and no tests/e2e/ spec) does not change the served +// app, so the 9-shard E2E matrix adds wall-time without coverage. e2e specs are +// excluded from the shortcut — changing an e2e spec REQUIRES running e2e. + +test("testsOnly: pure unit-test diff → true (still code)", () => { + const c = classifyPaths(["tests/unit/foo.test.ts", "tests/integration/bar.test.ts"]); + assert.equal(c.testsOnly, true); + assert.equal(c.code, true); +}); + +test("testsOnly: any non-test file flips it false", () => { + const c = classifyPaths(["tests/unit/foo.test.ts", "src/lib/db/core.ts"]); + assert.equal(c.testsOnly, false); +}); + +test("testsOnly: touching an e2e spec is NOT tests-only (e2e must run)", () => { + const c = classifyPaths(["tests/e2e/login.spec.ts"]); + assert.equal(c.testsOnly, false); +}); + +test("testsOnly: empty change list → false (fail-safe)", () => { + const c = classifyPaths([]); + assert.equal(c.testsOnly, false); }); diff --git a/tests/unit/claude-to-openai-system-role-6954.test.ts b/tests/unit/claude-to-openai-system-role-6954.test.ts new file mode 100644 index 0000000000..d9c48ac085 --- /dev/null +++ b/tests/unit/claude-to-openai-system-role-6954.test.ts @@ -0,0 +1,103 @@ +/** + * Tests for #6954 — mid-conversation system turns misattributed as assistant. + * + * `convertClaudeMessage` mapped any role that wasn't "user" or "tool" to + * "assistant", so a Claude message with `role: "system"` (e.g. an injected + * system reminder mid-conversation) was forwarded to OpenAI-format upstreams + * as an assistant turn — polluting the conversation history. + */ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { claudeToOpenAIRequest } = + await import("../../open-sse/translator/request/claude-to-openai.ts"); + +// --------------------------------------------------------------------------- +// 1. system message mid-conversation keeps role: "system" +// --------------------------------------------------------------------------- +test("mid-conversation system message preserves role:system (not assistant)", () => { + const result = claudeToOpenAIRequest( + "gpt-4o", + { + messages: [ + { role: "user", content: "hello" }, + { role: "assistant", content: "hi" }, + { role: "system", content: "Reminder: be concise." }, + { role: "user", content: "ok" }, + ], + }, + false + ); + + const roles = result.messages.map((m: { role: string }) => m.role); + assert.deepEqual(roles, ["user", "assistant", "system", "user"]); +}); + +// --------------------------------------------------------------------------- +// 2. system message with array content keeps role: "system" +// --------------------------------------------------------------------------- +test("system message with array content preserves role:system", () => { + const result = claudeToOpenAIRequest( + "gpt-4o", + { + messages: [ + { role: "user", content: "hello" }, + { + role: "system", + content: [{ type: "text", text: "System reminder text" }], + }, + ], + }, + false + ); + + const sysMsg = result.messages.find((m: { role: string }) => m.role === "system"); + assert.ok(sysMsg, "expected a system message in output"); + // Array content with text blocks is flattened to a string for system role + assert.equal( + typeof sysMsg.content === "string" ? sysMsg.content : JSON.stringify(sysMsg.content), + "System reminder text" + ); +}); + +// --------------------------------------------------------------------------- +// 3. top-level body.system still produces role: "system" (regression check) +// --------------------------------------------------------------------------- +test("body.system still produces role:system at index 0", () => { + const result = claudeToOpenAIRequest( + "gpt-4o", + { + system: "You are helpful.", + messages: [{ role: "user", content: "hi" }], + }, + false + ); + + assert.equal(result.messages[0].role, "system"); + assert.equal(result.messages[1].role, "user"); +}); + +// --------------------------------------------------------------------------- +// 4. assistant with tool_use still maps to assistant (regression check) +// --------------------------------------------------------------------------- +test("assistant role still maps to assistant (no regression)", () => { + const result = claudeToOpenAIRequest( + "gpt-4o", + { + messages: [ + { role: "user", content: "use the tool" }, + { + role: "assistant", + content: [ + { type: "text", text: "calling tool" }, + { type: "tool_use", id: "t1", name: "foo", input: {} }, + ], + }, + ], + }, + false + ); + + const roles = result.messages.map((m: { role: string }) => m.role); + assert.ok(roles.includes("assistant"), "assistant role must be preserved"); +}); diff --git a/tests/unit/cli-runtime.test.ts b/tests/unit/cli-runtime.test.ts index 46a3e3204c..293f6a3933 100644 --- a/tests/unit/cli-runtime.test.ts +++ b/tests/unit/cli-runtime.test.ts @@ -71,20 +71,60 @@ test("buildEnvWithRuntime preserva NODE_PATH existente", async () => { assert.ok(env.NODE_PATH.includes("/existing/path"), "NODE_PATH original deve ser preservado"); }); -test("isBetterSqliteBinaryValid detecta ELF magic bytes (Linux)", async () => { +test("isBetterSqliteBinaryValid rejeita binário com magic bytes válidos mas ABI incompatível (regressão #2493)", async () => { + // Regression for upstream 9router#2493: a binary that only "looks" native (correct ELF/Mach-O/PE + // header) but was built for a different Node ABI (NODE_MODULE_VERSION) must NOT be reported as + // valid — loading it crashes the process (segfault) instead of triggering a rebuild. const { getRuntimeNodeModules, isBetterSqliteBinaryValid } = await import("../../bin/cli/runtime/nativeDeps.mjs"); const nm = getRuntimeNodeModules(); const buildDir = join(nm, "better-sqlite3", "build", "Release"); mkdirSync(buildDir, { recursive: true }); const binary = join(buildDir, "better_sqlite3.node"); - const buf = Buffer.from([0x7f, 0x45, 0x4c, 0x46, 0x00, 0x00, 0x00, 0x00]); + const { platform } = await import("node:os"); + const os = platform(); + // Correct file-format magic bytes for the current OS, but not a real, loadable native addon — + // this is exactly what the old magic-bytes-only check let through. + const magicByPlatform = { + linux: [0x7f, 0x45, 0x4c, 0x46], + darwin: [0xcf, 0xfa, 0xed, 0xfe], + win32: [0x4d, 0x5a], + }; + const magic = magicByPlatform[os] ?? magicByPlatform.linux; + const buf = Buffer.concat([Buffer.from(magic), Buffer.alloc(64, 0)]); writeFileSync(binary, buf); const result = isBetterSqliteBinaryValid(); - const { platform } = await import("node:os"); - if (platform() === "linux") { - assert.equal(result, true, "ELF magic bytes devem ser válidos no Linux"); + assert.equal( + result, + false, + "binário com header válido mas ABI/conteúdo incompatível deve ser inválido" + ); + rmSync(join(nm, "better-sqlite3"), { recursive: true, force: true }); +}); + +test("isBetterSqliteBinaryValid aceita um binário nativo real e carregável", async () => { + const { getRuntimeNodeModules, isBetterSqliteBinaryValid } = + await import("../../bin/cli/runtime/nativeDeps.mjs"); + const { existsSync, copyFileSync } = await import("node:fs"); + const realBinary = join( + process.cwd(), + "node_modules", + "better-sqlite3", + "build", + "Release", + "better_sqlite3.node" + ); + if (!existsSync(realBinary)) { + // Ambient runtime without a compiled better-sqlite3 binary — nothing to assert here. + return; } + const nm = getRuntimeNodeModules(); + const buildDir = join(nm, "better-sqlite3", "build", "Release"); + mkdirSync(buildDir, { recursive: true }); + const binary = join(buildDir, "better_sqlite3.node"); + copyFileSync(realBinary, binary); + const result = isBetterSqliteBinaryValid(); + assert.equal(result, true, "um binário real, compatível com o Node atual, deve ser válido"); rmSync(join(nm, "better-sqlite3"), { recursive: true, force: true }); }); diff --git a/tests/unit/cloudflare-ai-neuron-exhaustion-6980.test.ts b/tests/unit/cloudflare-ai-neuron-exhaustion-6980.test.ts new file mode 100644 index 0000000000..4df5826c59 --- /dev/null +++ b/tests/unit/cloudflare-ai-neuron-exhaustion-6980.test.ts @@ -0,0 +1,108 @@ +/** + * Issue #6980 — Cloudflare Workers AI daily neuron exhaustion 429 must be + * classified as quota_exhausted (not transient rate_limit). + * + * Two layers of defense: + * 1. Provider-specific rule in providerErrorRules.ts → getProviderErrorRuleMatch + * 2. Global QUOTA_PATTERNS in classify429.ts → looksLikeQuotaExhausted + * + * Without these, the 429 body "you have used up your daily free allocation of + * 10,000 neurons" matches no keyword, falls through to rate_limit (~60s cooldown), + * and the combo router keeps cycling through every cloudflare model on retry + * against a budget that only resets at UTC midnight. + */ + +import { describe, test } from "node:test"; +import assert from "node:assert/strict"; + +import { + getProviderErrorRuleMatch, + providerRuleRegistry, +} from "../../open-sse/config/providerErrorRules.ts"; +import { classify429, looksLikeQuotaExhausted } from "../../src/shared/utils/classify429.ts"; + +// ─── Fixtures ──────────────────────────────────────────────────────────────── + +const CF_NEURON_BODY = + "you have used up your daily free allocation of 10,000 neurons, please upgrade to Cloudflare's Workers Paid plan"; + +const CF_NEURON_BODY_JSON = { + errors: [ + { + code: 4006, + message: + "you have used up your daily free allocation of 10,000 neurons, please upgrade to Cloudflare's Workers Paid plan", + }, + ], +}; + +// ─── Tests: provider-specific rule (primary path) ─────────────────────────── + +describe("#6980 provider rule: cloudflare-ai neuron exhaustion", () => { + test("cloudflare-ai is registered in providerRuleRegistry", () => { + assert.ok(providerRuleRegistry.has("cloudflare-ai")); + }); + + test("429 with plain-string neuron body → quota_exhausted, scope connection", () => { + const result = getProviderErrorRuleMatch("cloudflare-ai", 429, {}, CF_NEURON_BODY); + assert.ok(result, "expected a match"); + assert.equal(result!.reason, "quota_exhausted"); + assert.equal(result!.scope, "connection"); + // No explicit cooldownMs — recordModelLockoutFailure resolves to next UTC midnight. + assert.equal(result!.cooldownMs, undefined); + }); + + test("429 with JSON-structured neuron body → quota_exhausted", () => { + const result = getProviderErrorRuleMatch("cloudflare-ai", 429, {}, CF_NEURON_BODY_JSON); + assert.ok(result); + assert.equal(result!.reason, "quota_exhausted"); + assert.equal(result!.scope, "connection"); + }); + + test("non-429 status does not match even with neuron body", () => { + const result = getProviderErrorRuleMatch("cloudflare-ai", 500, {}, CF_NEURON_BODY); + assert.equal(result, null); + }); + + test("429 with unrelated body does not match", () => { + const result = getProviderErrorRuleMatch( + "cloudflare-ai", + 429, + {}, + { + error: "rate limited, try again later", + } + ); + assert.equal(result, null); + }); + + test("provider name matching is case-insensitive", () => { + const result = getProviderErrorRuleMatch("Cloudflare-AI", 429, {}, CF_NEURON_BODY); + assert.ok(result); + assert.equal(result!.reason, "quota_exhausted"); + }); +}); + +// ─── Tests: classify429 defense-in-depth (fallback path) ──────────────────── + +describe("#6980 classify429: daily free allocation pattern", () => { + test("looksLikeQuotaExhausted matches neuron body string", () => { + assert.ok(looksLikeQuotaExhausted(CF_NEURON_BODY)); + }); + + test("looksLikeQuotaExhausted matches neuron body JSON-stringified", () => { + assert.ok(looksLikeQuotaExhausted(CF_NEURON_BODY_JSON)); + }); + + test("classify429 returns quota_exhausted for neuron body", () => { + assert.equal(classify429({ status: 429, body: CF_NEURON_BODY }), "quota_exhausted"); + }); + + test("classify429 returns quota_exhausted for neuron JSON body", () => { + assert.equal(classify429({ status: 429, body: CF_NEURON_BODY_JSON }), "quota_exhausted"); + }); + + test("classify429 returns rate_limit for generic 429 without quota keywords", () => { + assert.equal(classify429({ status: 429, body: "Too many requests" }), "rate_limit"); + }); +}); diff --git a/tests/unit/codex-tools-regex-lookaround.test.ts b/tests/unit/codex-tools-regex-lookaround.test.ts new file mode 100644 index 0000000000..4778ec7ac7 --- /dev/null +++ b/tests/unit/codex-tools-regex-lookaround.test.ts @@ -0,0 +1,74 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; + +import { normalizeCodexTools } from "../../open-sse/executors/codex/tools.ts"; + +// Port of 9router#1556: OpenAI/Codex Responses API rejects JSON Schema `pattern` +// fields containing regex lookaround (lookahead/lookbehind) with: +// "Invalid JSON schema: regex lookaround is not supported. Found at $.properties.email.pattern." +// Clients (e.g. IDE agent harnesses) commonly emit lookahead patterns such as +// `^(?=.*@).+$` for "must contain an @". These must be stripped before the +// tool schema reaches the Codex/OpenAI Responses API. +test("normalizeCodexTools strips regex lookaround from function tool parameter patterns", () => { + const body: Record = { + tools: [ + { + type: "function", + function: { + name: "send_email", + description: "Send an email", + parameters: { + type: "object", + properties: { + email: { + type: "string", + pattern: "^(?=.*@).+$", + }, + }, + }, + }, + }, + ], + }; + + normalizeCodexTools(body); + + const tools = body.tools as Array>; + const parameters = tools[0].parameters as Record; + const properties = parameters.properties as Record; + const emailSchema = properties.email as Record; + + assert.equal( + emailSchema.pattern, + undefined, + "lookaround pattern must be stripped, not forwarded upstream" + ); +}); + +test("normalizeCodexTools preserves plain (non-lookaround) regex patterns", () => { + const body: Record = { + tools: [ + { + type: "function", + function: { + name: "send_email", + parameters: { + type: "object", + properties: { + zip: { type: "string", pattern: "^[0-9]{5}$" }, + }, + }, + }, + }, + ], + }; + + normalizeCodexTools(body); + + const tools = body.tools as Array>; + const parameters = tools[0].parameters as Record; + const properties = parameters.properties as Record; + const zipSchema = properties.zip as Record; + + assert.equal(zipSchema.pattern, "^[0-9]{5}$"); +}); diff --git a/tests/unit/combo-diagnostics-trace.test.ts b/tests/unit/combo-diagnostics-trace.test.ts index be25e64c77..fe8bb546a4 100644 --- a/tests/unit/combo-diagnostics-trace.test.ts +++ b/tests/unit/combo-diagnostics-trace.test.ts @@ -79,3 +79,41 @@ test("combo diagnostics: secret containment — non-whitelisted fields never sur assert.ok(!serialized.includes("accessToken"), "no accessToken KEY survives"); assert.ok(!serialized.includes("token"), "no token KEY survives"); }); + +test("combo diagnostics: terminalReason with a non-Latin1 char (em dash) must not crash Response construction (#6612)", () => { + const terminalReason = "reasoning consumed 5/5 tokens — no content output"; + assert.doesNotThrow(() => { + const res = errorResponseWithComboDiagnostics( + 502, + `Upstream response failed quality validation: ${terminalReason}`, + { + poolSize: 4, + attempted: 1, + excluded: [{ provider: "deepseek", model: "deepseek-v4-flash-free", reason: "quality — bad" }], + attemptOrder: [{ provider: "deepseek", model: "deepseek-v4-flash-free" }], + terminalReason, + } + ); + assert.equal(res.status, 502); + }); +}); + +test("combo diagnostics: JSON body keeps the original non-Latin1 text even though headers are ASCII-sanitized (#6612)", async () => { + const terminalReason = "reasoning consumed 5/5 tokens — no content output"; + const res = errorResponseWithComboDiagnostics( + 502, + `Upstream response failed quality validation: ${terminalReason}`, + { + poolSize: 1, + attempted: 1, + excluded: [], + attemptOrder: [{ provider: "deepseek", model: "deepseek-v4-flash-free" }], + terminalReason, + } + ); + // Header value must be a valid Latin1 ByteString — em dash (U+2014) replaced. + assert.equal(res.headers.get("x-omniroute-combo-terminal-reason"), terminalReason.replace("—", "?")); + const body = await res.json(); + // JSON body keeps the original, readable (unsanitized) em dash. + assert.equal(body.diagnostics.terminalReason, terminalReason); +}); diff --git a/tests/unit/combo-proxy-assignments-parse-7149.test.ts b/tests/unit/combo-proxy-assignments-parse-7149.test.ts new file mode 100644 index 0000000000..7afb82b487 --- /dev/null +++ b/tests/unit/combo-proxy-assignments-parse-7149.test.ts @@ -0,0 +1,32 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { parseComboProxyAssignmentIds } from "../../src/app/(dashboard)/dashboard/combos/useComboProxyAssignments.ts"; + +test("#7149: parseComboProxyAssignmentIds extracts scopeIds from valid combo assignments", () => { + const data = { + items: [ + { scopeId: "combo-1", proxyId: "proxy-1", scope: "combo" }, + { scopeId: "combo-2", proxyId: "proxy-2", scope: "combo" }, + ], + }; + assert.deepEqual(parseComboProxyAssignmentIds(data), ["combo-1", "combo-2"]); +}); + +test("#7149: parseComboProxyAssignmentIds drops entries missing scopeId or proxyId", () => { + const data = { + items: [ + { scopeId: "combo-1", proxyId: "proxy-1" }, + { scopeId: "combo-2", proxyId: null }, + { scopeId: null, proxyId: "proxy-3" }, + {}, + ], + }; + assert.deepEqual(parseComboProxyAssignmentIds(data), ["combo-1"]); +}); + +test("#7149: parseComboProxyAssignmentIds returns [] for missing/malformed items", () => { + assert.deepEqual(parseComboProxyAssignmentIds(null), []); + assert.deepEqual(parseComboProxyAssignmentIds(undefined), []); + assert.deepEqual(parseComboProxyAssignmentIds({}), []); + assert.deepEqual(parseComboProxyAssignmentIds({ items: "not-an-array" }), []); +}); diff --git a/tests/unit/combo-scope-proxy-dead-7149.test.ts b/tests/unit/combo-scope-proxy-dead-7149.test.ts new file mode 100644 index 0000000000..8d301763e9 --- /dev/null +++ b/tests/unit/combo-scope-proxy-dead-7149.test.ts @@ -0,0 +1,87 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-combo-proxy-7149-")); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.API_KEY_SECRET = "test-secret"; + +const core = await import("../../src/lib/db/core.ts"); +const proxiesDb = await import("../../src/lib/db/proxies.ts"); +const providersDb = await import("../../src/lib/db/providers.ts"); +const combosDb = await import("../../src/lib/db/combos.ts"); +const settingsDb = await import("../../src/lib/db/settings.ts"); + +type ProxyResolutionLike = { + proxy?: { host?: string } | null; + level?: string; + levelId?: string | null; +} | null; + +async function resetStorage() { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); +} + +test.after(async () => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +test("#7149: a proxy assigned to a Combo via the dashboard (registry scope='combo') is honored when resolving the proxy for a request routed through that combo", async () => { + await resetStorage(); + + const comboProxy = await proxiesDb.createProxy({ + name: "Combo-Assigned Proxy", + type: "http", + host: "10.20.30.40", + port: 8888, + }); + assert.ok(comboProxy?.id); + + const combo = await combosDb.createCombo({ + name: "diy_deepseek-v4-flash", + strategy: "round-robin", + models: ["openai/gpt-4"], + }); + const comboRecord = combo as Record; + assert.ok(comboRecord?.id); + const comboId = comboRecord.id as string; + + const assignment = await proxiesDb.assignProxyToScope("combo", comboId, comboProxy!.id); + assert.ok(assignment, "assignProxyToScope('combo', ...) should persist the assignment"); + + const directRegistryLookup = (await proxiesDb.resolveProxyForScopeFromRegistry( + "combo", + comboId + )) as ProxyResolutionLike; + assert.ok( + directRegistryLookup?.proxy, + "the registry must be able to answer a direct combo-scope lookup" + ); + assert.equal(directRegistryLookup?.proxy?.host, "10.20.30.40"); + + const connection = await providersDb.createProviderConnection({ + provider: "openai", + authType: "apikey", + apiKey: "sk-test-1234", + name: "openai-account-1", + }); + const connectionRecord = connection as Record | null; + const connectionId = connectionRecord?.id as string; + assert.ok(connectionId, "test setup requires a real connection id"); + + const resolved = (await settingsDb.resolveProxyForConnection( + connectionId + )) as ProxyResolutionLike; + + assert.equal( + resolved?.level, + "combo", + `expected the combo-assigned proxy to be resolved (level="combo"), got level="${resolved?.level}" — the registry-based combo proxy assignment is never consulted by resolveProxyForConnection()` + ); + assert.equal(resolved?.proxy?.host, "10.20.30.40"); +}); diff --git a/tests/unit/composer-tool-calls.test.ts b/tests/unit/composer-tool-calls.test.ts index c1133f0513..5fba8abd3a 100644 --- a/tests/unit/composer-tool-calls.test.ts +++ b/tests/unit/composer-tool-calls.test.ts @@ -214,3 +214,27 @@ test("feedStreamingChunk: noop after done state", () => { assert.equal(out.safeDelta, ""); assert.equal(out.ready, false); }); + +// ─── Regression: space-separated arg name/value (9router#1811) ─────────────── +// Cursor's real Composer/Auto output has been observed using a single space +// (instead of a newline) between the arg name and its value inside a +// <|tool▁sep|> segment, e.g. "<|tool▁sep|>path /Users/.../test". The parser +// must still extract {path: "/Users/.../test"} rather than treating the whole +// segment as the (empty-valued) arg name. +test("parseComposerToolCalls: parses args separated by a space instead of a newline (Cursor Composer live capture)", () => { + const text = + "<|tool▁calls▁begin|><|tool▁call▁begin|> Write " + + "<|tool▁sep|>path /Users/kabawagang/Desktop/Code/iOS_Review/test " + + "<|tool▁sep|>contents 22\n\n<|tool▁call▁end|><|tool▁calls▁end|>"; + + const result = parseComposerToolCalls(text); + + assert.equal(result.toolCalls.length, 1); + const tc = result.toolCalls[0]; + assert.equal(tc.function.name, "Write"); + const args = JSON.parse(tc.function.arguments); + assert.deepEqual(args, { + path: "/Users/kabawagang/Desktop/Code/iOS_Review/test", + contents: 22, + }); +}); diff --git a/tests/unit/compression/adaptive-context-budget-config.test.ts b/tests/unit/compression/adaptive-context-budget-config.test.ts new file mode 100644 index 0000000000..4dd4acb68c --- /dev/null +++ b/tests/unit/compression/adaptive-context-budget-config.test.ts @@ -0,0 +1,84 @@ +// Regression test for #7005 — adaptive context-budget dial not configurable. +// +// The compute engine for the adaptive context-budget ("dial") shipped in PR #4716 +// (Phase 4C), but it was never wired to persistence or the API: the PUT schema +// rejected any `contextBudget` payload (strict schema, no such key) and the DB-backed +// GET path never surfaced a `contextBudget` field. This test proves both halves of +// the wiring: the Zod schema accepts a `contextBudget` write, and the DB read/write +// path round-trips it. +import { describe, it, beforeEach, afterEach, after } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const TEST_DATA_DIR = fs.mkdtempSync( + path.join(os.tmpdir(), "omniroute-adaptive-context-budget-db-") +); +const ORIGINAL_DATA_DIR = process.env.DATA_DIR; +process.env.DATA_DIR = TEST_DATA_DIR; + +const core = await import("../../../src/lib/db/core.ts"); +const { getCompressionSettings, updateCompressionSettings } = await import( + "../../../src/lib/db/compression.ts" +); +const { compressionSettingsUpdateSchema } = await import( + "../../../src/shared/validation/compressionConfigSchemas.ts" +); +const { DEFAULT_CONTEXT_BUDGET } = await import( + "../../../open-sse/services/compression/adaptiveCompression/types.ts" +); + +beforeEach(() => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); +}); + +afterEach(() => { + core.resetDbInstance(); +}); + +after(() => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + if (ORIGINAL_DATA_DIR === undefined) { + delete process.env.DATA_DIR; + } else { + process.env.DATA_DIR = ORIGINAL_DATA_DIR; + } +}); + +describe("bug #7005: adaptive context-budget dial is configurable", () => { + it("compressionSettingsUpdateSchema accepts a contextBudget write", () => { + const result = compressionSettingsUpdateSchema.safeParse({ + contextBudget: { + mode: "floor", + policy: "percentage", + outputReserve: 2048, + safetyMargin: 512, + pct: 0.75, + absoluteBudget: 0, + }, + }); + assert.equal(result.success, true, JSON.stringify("error" in result ? result.error : null)); + }); + + it("getCompressionSettings() defaults contextBudget to DEFAULT_CONTEXT_BUDGET when absent", async () => { + const settings = await getCompressionSettings(); + assert.deepEqual(settings.contextBudget, DEFAULT_CONTEXT_BUDGET); + }); + + it("updateCompressionSettings() persists a partial contextBudget merge", async () => { + await updateCompressionSettings({ + contextBudget: { ...DEFAULT_CONTEXT_BUDGET, mode: "floor", policy: "absolute", absoluteBudget: 8000 }, + }); + const settings = await getCompressionSettings(); + assert.equal(settings.contextBudget?.mode, "floor"); + assert.equal(settings.contextBudget?.policy, "absolute"); + assert.equal(settings.contextBudget?.absoluteBudget, 8000); + // Untouched fields keep their defaults (this is a JSON-column replace like ultra/aggressive, + // not a deep merge — the caller sends the full object, mirroring the existing pattern). + assert.equal(settings.contextBudget?.outputReserve, DEFAULT_CONTEXT_BUDGET.outputReserve); + }); +}); diff --git a/tests/unit/compression/headroom-developer-role-2132.test.ts b/tests/unit/compression/headroom-developer-role-2132.test.ts new file mode 100644 index 0000000000..5f797d3f1a --- /dev/null +++ b/tests/unit/compression/headroom-developer-role-2132.test.ts @@ -0,0 +1,91 @@ +/** + * Regression test for upstream 9router#2132 (ported): "Token saver Headroom ruins plan mode + * in Codex CLI". + * + * Root cause: SmartCrusher's system-message guard only checked `role === "system"`. Codex CLI + * (open-sse/executors/codex.ts) sends its instructions/tool-schema turn with role "developer" + * (the Responses-API equivalent of "system" used by newer models). Every other guard in this + * codebase that excludes "system" also excludes "developer" (see roleNormalizer.ts, + * contextManager.ts, claudeUpstreamMessages.ts, etc.) — SmartCrusher was the exception, so it + * happily tabular-compacted JSON arrays (e.g. the update_plan tool schema/examples) embedded in + * the developer-role turn, corrupting the instructions the model needs to call the plan tool. + */ + +import { describe, it, before } from "node:test"; +import assert from "node:assert/strict"; + +let crushMessages: typeof import("../../../open-sse/services/compression/engines/headroom/smartcrusher.ts").crushMessages; +let collectCompactableArrays: typeof import("../../../open-sse/services/compression/engines/headroom/smartcrusher.ts").collectCompactableArrays; +let headroomEngine: import("../../../open-sse/services/compression/engines/headroom/index.ts").headroomEngine; + +before(async () => { + const mod = await import("../../../open-sse/services/compression/engines/headroom/smartcrusher.ts"); + crushMessages = mod.crushMessages; + collectCompactableArrays = mod.collectCompactableArrays; + + const engineMod = await import("../../../open-sse/services/compression/engines/headroom/index.ts"); + headroomEngine = engineMod.headroomEngine; +}); + +/** A homogeneous array big enough (>= default minRows=8) to trigger compaction. */ +function makePlanSchemaExample(): Record[] { + return Array.from({ length: 10 }, (_, i) => ({ + step: `step-${i + 1}`, + status: i === 0 ? "in_progress" : "pending", + })); +} + +describe("headroom SmartCrusher — developer-role guard (9router#2132)", () => { + it("does NOT compact JSON arrays embedded in a developer-role message (crushMessages)", () => { + const json = JSON.stringify(makePlanSchemaExample()); + const messages = [ + { + role: "developer", + content: `Use the update_plan tool. Example plan:\n\`\`\`json\n${json}\n\`\`\``, + }, + { role: "user", content: "Refactor the auth module." }, + ]; + + const { messages: result, changed } = crushMessages(messages, 8); + + assert.equal(changed, false, "developer-role content must not be touched"); + assert.equal(result[0].content, messages[0].content); + }); + + it("still compacts the same payload when placed under role: system (control case)", () => { + // Sanity check: this proves the array itself WOULD be compactable — the guard, not the + // shape of the payload, is what must change. + const json = JSON.stringify(makePlanSchemaExample()); + const messages = [{ role: "user", content: `\`\`\`json\n${json}\n\`\`\`` }]; + + const { changed } = crushMessages(messages, 8); + assert.equal(changed, true, "control case: user-role content of the same shape IS compacted"); + }); + + it("collectCompactableArrays does not surface arrays from developer-role messages", () => { + const json = JSON.stringify(makePlanSchemaExample()); + const messages = [ + { role: "developer", content: `\`\`\`json\n${json}\n\`\`\`` }, + ]; + const found = collectCompactableArrays(messages, 8); + assert.equal(found.length, 0); + }); + + it("headroomEngine.apply leaves a Codex-CLI-shaped developer turn untouched end-to-end", () => { + const json = JSON.stringify(makePlanSchemaExample()); + const body: Record = { + model: "gpt-5-codex", + messages: [ + { + role: "developer", + content: `Instructions with an embedded schema example:\n\`\`\`json\n${json}\n\`\`\``, + }, + { role: "user", content: "Implement the feature." }, + ], + }; + + const result = headroomEngine.apply(body); + assert.equal(result.compressed, false); + assert.deepEqual(result.body, body); + }); +}); diff --git a/tests/unit/duckduckgo-vqd-429-misclassification-6996.test.ts b/tests/unit/duckduckgo-vqd-429-misclassification-6996.test.ts new file mode 100644 index 0000000000..8b00ac5dc3 --- /dev/null +++ b/tests/unit/duckduckgo-vqd-429-misclassification-6996.test.ts @@ -0,0 +1,99 @@ +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-6996-")); +process.env.DATA_DIR = TEST_DATA_DIR; + +const { DuckDuckGoWebExecutor, STATUS_URL } = await import( + "../../open-sse/executors/duckduckgo-web.ts" +); +const { resetDbInstance } = await import("../../src/lib/db/core.ts"); +const executeInputBase = { + model: "gpt-4o-mini", + body: { + model: "gpt-4o-mini", + messages: [{ role: "user", content: "hi" }], + stream: false, + }, + stream: false, + credentials: {}, +}; + +describe("#6996 DuckDuckGo VQD 429 misclassification", () => { + let originalFetch: typeof fetch; + + before(() => { + originalFetch = globalThis.fetch; + }); + + after(() => { + globalThis.fetch = originalFetch; + resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + }); + + it("propagates upstream 429 instead of masking it as a generic 503", async () => { + // Set the mock AFTER the module import so it wins over + // open-sse/utils/proxyFetch.ts's own module-load-time + // `globalThis.fetch = patchedFetch` side effect. + globalThis.fetch = (async (input: RequestInfo | URL) => { + const url = typeof input === "string" ? input : (input as URL | Request).toString(); + if (url === STATUS_URL) { + return new Response("", { + status: 429, + headers: { "Retry-After": "30" }, + }); + } + if (url.includes("/duckchat/v1/chat")) { + throw new Error("unexpected chat POST reached without a VQD token"); + } + return new Response("", { status: 200 }); + }) as typeof fetch; + + const executor = new DuckDuckGoWebExecutor(); + const response = await executor.execute(executeInputBase); + + const httpResponse = + response instanceof Response + ? response + : (response as { response: Response }).response; + const bodyText = await httpResponse.text(); + + assert.equal( + httpResponse.status, + 429, + `expected the executor to surface DuckDuckGo's real 429 rate-limit status, got ${httpResponse.status} (body: ${bodyText})` + ); + }); + + it("still returns 503 fallback for a genuine 5xx status on the VQD endpoint", async () => { + globalThis.fetch = (async (input: RequestInfo | URL) => { + const url = typeof input === "string" ? input : (input as URL | Request).toString(); + if (url === STATUS_URL) { + return new Response("", { status: 500 }); + } + if (url.includes("/duckchat/v1/chat")) { + throw new Error("unexpected chat POST reached without a VQD token"); + } + return new Response("", { status: 200 }); + }) as typeof fetch; + + const executor = new DuckDuckGoWebExecutor(); + const response = await executor.execute(executeInputBase); + + const httpResponse = + response instanceof Response + ? response + : (response as { response: Response }).response; + const bodyText = await httpResponse.text(); + + assert.equal( + httpResponse.status, + 503, + `expected the executor to keep the 503 fallback for a genuine upstream 5xx, got ${httpResponse.status} (body: ${bodyText})` + ); + }); +}); diff --git a/tests/unit/early-stream-keepalive.test.ts b/tests/unit/early-stream-keepalive.test.ts index a75c4c2764..5cd04ca192 100644 --- a/tests/unit/early-stream-keepalive.test.ts +++ b/tests/unit/early-stream-keepalive.test.ts @@ -4,6 +4,7 @@ import assert from "node:assert/strict"; import { withEarlyStreamKeepalive, ANTHROPIC_PING_FRAME, + OPENAI_KEEPALIVE_FRAME, } from "../../open-sse/utils/earlyStreamKeepalive.ts"; async function readAll(response: Response): Promise { @@ -68,9 +69,40 @@ test("ANTHROPIC_PING_FRAME is a real Anthropic ping event (not a comment)", () = assert.doesNotMatch(decoded, /^:/, "must not be an SSE comment"); }); +test("OPENAI_KEEPALIVE_FRAME is a JSON-parseable OpenAI streaming chunk", () => { + const decoded = new TextDecoder().decode(OPENAI_KEEPALIVE_FRAME); + assert.match(decoded, /^data: /); + assert.doesNotMatch(decoded, /^:/, "must not be an SSE comment"); + + const payload = JSON.parse(decoded.slice("data: ".length).trim()); + assert.equal(payload.object, "chat.completion.chunk"); + assert.deepEqual(payload.choices, [{ index: 0, delta: {}, finish_reason: null }]); +}); + +test("slow handler emits the custom OpenAI keepalive chunk before the body", async () => { + const slow = new Promise((resolve) => { + setTimeout(() => resolve(sseResponse("data: [DONE]\n\n")), 120); + }); + + const result = await withEarlyStreamKeepalive(slow, { + thresholdMs: 25, + intervalMs: 20, + keepaliveFrame: OPENAI_KEEPALIVE_FRAME, + }); + + const body = await readAll(result); + assert.doesNotMatch(body, /: omniroute-keepalive/); + const firstFrame = body.split("\n\n")[0]; + assert.doesNotThrow(() => JSON.parse(firstFrame.slice("data: ".length))); + assert.match(body, /data: \[DONE\]/); +}); + test("slow handler emits the custom keepaliveFrame (Anthropic ping) before the body", async () => { const slow = new Promise((resolve) => { - setTimeout(() => resolve(sseResponse("event: message_start\ndata: {}\n\ndata: [DONE]\n\n")), 120); + setTimeout( + () => resolve(sseResponse("event: message_start\ndata: {}\n\ndata: [DONE]\n\n")), + 120 + ); }); const result = await withEarlyStreamKeepalive(slow, { diff --git a/tests/unit/free-pool-tab.test.tsx b/tests/unit/free-pool-tab.test.tsx index 6279174fa4..91ee249776 100644 --- a/tests/unit/free-pool-tab.test.tsx +++ b/tests/unit/free-pool-tab.test.tsx @@ -91,13 +91,13 @@ afterEach(() => { // ── Tests ───────────────────────────────────────────────────────────────────── describe("FreePoolTab source toggles", () => { - it("renders a toggle group with exactly 3 buttons", async () => { + it("renders a toggle group with exactly 4 buttons", async () => { const el = renderTab(); await waitForCondition(() => el.querySelector("[role='group']") !== null); const bar = el.querySelector("[role='group']")!; expect(bar).toBeTruthy(); const buttons = bar.querySelectorAll("button"); - expect(buttons.length).toBe(3); + expect(buttons.length).toBe(4); }); it("all toggles start enabled (aria-pressed=true)", async () => { @@ -160,7 +160,7 @@ describe("FreePoolTab source toggles", () => { expect(stored).toContain("1proxy"); }); - it("button labels are 1proxy, Proxifly, IPLocate", async () => { + it("button labels are 1proxy, Proxifly, IPLocate, Webshare", async () => { const el = renderTab(); await waitForCondition(() => el.querySelector("[role='group']") !== null); const texts = Array.from(el.querySelector("[role='group']")!.querySelectorAll("button")).map( @@ -169,6 +169,7 @@ describe("FreePoolTab source toggles", () => { expect(texts).toContain("1proxy"); expect(texts).toContain("Proxifly"); expect(texts).toContain("IPLocate"); + expect(texts).toContain("Webshare"); }); }); diff --git a/tests/unit/fusion-panel-size-cap-1905.test.ts b/tests/unit/fusion-panel-size-cap-1905.test.ts new file mode 100644 index 0000000000..6b9ac1b935 --- /dev/null +++ b/tests/unit/fusion-panel-size-cap-1905.test.ts @@ -0,0 +1,79 @@ +/** + * Regression test for upstream issue decolua/9router#1905. + * + * Reported symptom: a fusion combo populated with ~70+ panel models fans every + * member out in parallel (`open-sse/services/fusion.ts::handleFusionChat` → + * `Promise.all`-style fan-out via `collectPanel`), buffering each model's full + * response text in memory at once. With the runtime heap capped at 1024MB + * (Dockerfile `OMNIROUTE_MEMORY_MB`), a large panel with sizable concurrent + * responses can exceed the heap ceiling and crash the whole container with + * "FATAL ERROR: Ineffective mark-compacts near heap limit — JavaScript heap + * out of memory" instead of failing one request gracefully. + * + * Fix: `handleFusionChat` now rejects panels above a configurable hard cap + * (`FUSION_DEFAULTS.maxPanel`, overridable via `fusionTuning.maxPanel`) with a + * clean 400 *before* fan-out, rather than let an unbounded panel size drive + * the process into an OOM crash. + */ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { handleFusionChat, FUSION_DEFAULTS } from "../../open-sse/services/fusion.ts"; + +const noop = () => {}; +const log = { info: noop, warn: noop, debug: noop, error: noop }; + +type Body = Record; + +test("fusion #1905: an oversized panel (73 models) is rejected before fan-out instead of OOM-crashing", async () => { + let calls = 0; + const handleSingleModel = (_b: Body, _m: string) => { + calls++; + const body = JSON.stringify({ + choices: [{ message: { role: "assistant", content: "x".repeat(1000) } }], + }); + return Promise.resolve( + new Response(body, { status: 200, headers: { "Content-Type": "application/json" } }) + ); + }; + + const panel = Array.from({ length: 73 }, (_, i) => `provider/model-${i}`); + + const res = await handleFusionChat({ + body: { messages: [{ role: "user", content: "hi" }] }, + models: panel, + handleSingleModel, + log, + comboName: "auto", + }); + + assert.equal(res.status, 400); + // Must reject BEFORE fan-out — no per-model calls should have happened. + assert.equal(calls, 0, "panel fan-out must not start once the size cap is exceeded"); + + const json = (await res.json()) as { error?: { message?: string } }; + assert.match(json.error?.message ?? "", /panel/i); +}); + +test("fusion #1905: a panel at or under the cap still fans out normally", async () => { + const handleSingleModel = (_b: Body, _m: string) => { + const body = JSON.stringify({ + choices: [{ message: { role: "assistant", content: "ok" } }], + }); + return Promise.resolve( + new Response(body, { status: 200, headers: { "Content-Type": "application/json" } }) + ); + }; + + const panel = Array.from({ length: FUSION_DEFAULTS.maxPanel }, (_, i) => `provider/model-${i}`); + + const res = await handleFusionChat({ + body: { messages: [{ role: "user", content: "hi" }] }, + models: panel, + handleSingleModel, + log, + comboName: "auto", + }); + + assert.equal(res.status, 200); +}); diff --git a/tests/unit/gemini-web.test.ts b/tests/unit/gemini-web.test.ts index 5e4a25d0cf..8ae1fe9675 100644 --- a/tests/unit/gemini-web.test.ts +++ b/tests/unit/gemini-web.test.ts @@ -266,15 +266,20 @@ test("#2832: GeminiWebExecutor catch block sanitizes Playwright launch errors (i // ─── StreamGenerate parsing ───────────────────────────────────────────────── -test("parseStreamResponse concatenates Gemini Web text from multiple wrb.fr chunks", () => { +test("parseStreamResponse keeps only the final cumulative StreamGenerate snapshot (no duplication) — regression for #7163", () => { const makeChunk = (text: string) => { const inner = new Array(80).fill(null); inner[4] = [[null, [text]]]; return `[["wrb.fr", null, ${JSON.stringify(JSON.stringify(inner))}]]`; }; - const raw = `)]}'\n10\n${makeChunk("First ")}\n5\n${makeChunk("chunk")}`; - assert.equal(parseStreamResponse(raw), "First chunk"); + // Gemini's StreamGenerate frames are CUMULATIVE snapshots: each later frame + // repeats the full answer generated so far, not just the new characters. + const frame1 = "Hello!"; + const frame2 = "Hello! How can I"; + const frame3 = "Hello! How can I help you out today?"; + const raw = `)]}'\n10\n${makeChunk(frame1)}\n5\n${makeChunk(frame2)}\n5\n${makeChunk(frame3)}`; + assert.equal(parseStreamResponse(raw), frame3); }); test("parseStreamResponse ignores wrb.fr lines whose first entry is not an array", () => { diff --git a/tests/unit/homolog-admin-client.test.ts b/tests/unit/homolog-admin-client.test.ts new file mode 100644 index 0000000000..225f66073c --- /dev/null +++ b/tests/unit/homolog-admin-client.test.ts @@ -0,0 +1,17 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { extractJwtCookie, extractApiKey } from "../../scripts/homolog/lib/adminClient.mjs"; + +test("extrai o cookie JWT do set-cookie do login", () => { + const jwt = extractJwtCookie(["auth_token=abc.def.ghi; Path=/; HttpOnly; SameSite=Lax"]); + assert.equal(jwt, "auth_token=abc.def.ghi"); +}); + +test("retorna null sem set-cookie de token", () => { + assert.equal(extractJwtCookie(["other=1; Path=/"]), null); +}); + +test("extrai key e id do POST /api/keys", () => { + const r = extractApiKey({ key: "or-abc123", id: "k1", name: "homolog-run" }); + assert.deepEqual(r, { key: "or-abc123", id: "k1" }); +}); diff --git a/tests/unit/homolog-parity.test.ts b/tests/unit/homolog-parity.test.ts new file mode 100644 index 0000000000..0c3d779f41 --- /dev/null +++ b/tests/unit/homolog-parity.test.ts @@ -0,0 +1,29 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { evaluateParity } from "../../scripts/homolog/lib/parity.mjs"; + +test("parity OK quando health bate com a versão esperada", () => { + const r = evaluateParity( + { status: "healthy", version: "3.8.49" }, + { expectedVersion: "3.8.49", httpStatus: 200 } + ); + assert.equal(r.ok, true); + assert.deepEqual(r.failures, []); +}); + +test("parity falha listando cada divergência", () => { + const r = evaluateParity( + { status: "degraded", version: "3.8.47" }, + { expectedVersion: "3.8.49", httpStatus: 200 } + ); + assert.equal(r.ok, false); + assert.equal(r.failures.length, 2); // status!=healthy, version mismatch +}); + +test("parity falha em HTTP não-200 mesmo com body bom", () => { + const r = evaluateParity( + { status: "healthy", version: "3.8.49" }, + { expectedVersion: "3.8.49", httpStatus: 503 } + ); + assert.equal(r.ok, false); +}); diff --git a/tests/unit/homolog-promptfoo-ctrf.test.ts b/tests/unit/homolog-promptfoo-ctrf.test.ts new file mode 100644 index 0000000000..d22dd652ee --- /dev/null +++ b/tests/unit/homolog-promptfoo-ctrf.test.ts @@ -0,0 +1,18 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { promptfooToCtrf } from "../../scripts/homolog/lib/promptfooToCtrf.mjs"; + +test("mapeia resultados do promptfoo para tests CTRF", () => { + const ctrf = promptfooToCtrf({ + results: { + results: [ + { provider: { label: "openai" }, success: true, latencyMs: 812 }, + { provider: { label: "grok" }, success: false, latencyMs: 30000, error: "timeout" }, + ], + }, + }); + assert.equal(ctrf.results.summary.tests, 2); + assert.equal(ctrf.results.summary.passed, 1); + assert.equal(ctrf.results.tests[1].status, "failed"); + assert.equal(ctrf.results.tests[1].name, "provider-smoke: grok"); +}); diff --git a/tests/unit/homolog-provider-tiers.test.ts b/tests/unit/homolog-provider-tiers.test.ts new file mode 100644 index 0000000000..3c793da73f --- /dev/null +++ b/tests/unit/homolog-provider-tiers.test.ts @@ -0,0 +1,24 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { pickSmokeModels } from "../../scripts/homolog/lib/providerTiers.mjs"; + +const CATALOG = [ + { id: "openai/gpt-5-mini" }, + { id: "openai/gpt-5" }, + { id: "anthropic/claude-sonnet-5" }, + { id: "mistral/mistral-small" }, + { id: "grok/grok-4-fast" }, +]; + +test("1 modelo por provider crítico (o primeiro do catálogo)", () => { + const picks = pickSmokeModels(CATALOG, ["openai", "anthropic", "grok"]); + assert.deepEqual( + picks.map((p) => p.model), + ["openai/gpt-5-mini", "anthropic/claude-sonnet-5", "grok/grok-4-fast"] + ); +}); + +test("provider crítico ausente do catálogo vira miss reportável", () => { + const picks = pickSmokeModels(CATALOG, ["openai", "nvidia"]); + assert.equal(picks.find((p) => p.provider === "nvidia").model, null); +}); diff --git a/tests/unit/homolog-sse-parser.test.ts b/tests/unit/homolog-sse-parser.test.ts new file mode 100644 index 0000000000..b51c226385 --- /dev/null +++ b/tests/unit/homolog-sse-parser.test.ts @@ -0,0 +1,28 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { parseSseChunk, summarizeStream } from "../../scripts/homolog/lib/sseCheck.mjs"; + +test("parseSseChunk separa eventos data: e detecta [DONE]", () => { + const events = parseSseChunk('data: {"choices":[{"delta":{"content":"O"}}]}\n\ndata: [DONE]\n\n'); + assert.equal(events.length, 2); + assert.equal(events[1], "[DONE]"); +}); + +test("parseSseChunk acha data: mesmo precedido de comment-lines SSE no mesmo bloco", () => { + // Formato real da VPS (v3.8.47): trailers de telemetria como comments (`: x-omniroute-*`) + // no MESMO bloco do data: [DONE] — o parser não pode olhar só o início do bloco. + const chunk = + 'data: {"choices":[{"delta":{"content":"OK"}}]}\n\n' + + ": x-omniroute-cache-hit=false\n: x-omniroute-latency-ms=67\ndata: [DONE]\n\n"; + const events = parseSseChunk(chunk); + assert.deepEqual(events, ['{"choices":[{"delta":{"content":"OK"}}]}', "[DONE]"]); +}); + +test("summarizeStream exige >=1 delta de conteúdo e terminador [DONE]", () => { + const good = summarizeStream(['{"choices":[{"delta":{"content":"OK"}}]}', "[DONE]"]); + assert.equal(good.ok, true); + const noDone = summarizeStream(['{"choices":[{"delta":{"content":"OK"}}]}']); + assert.equal(noDone.ok, false); + const noContent = summarizeStream(["[DONE]"]); + assert.equal(noContent.ok, false); +}); diff --git a/tests/unit/image-model-not-in-chat-catalog-6457.test.ts b/tests/unit/image-model-not-in-chat-catalog-6457.test.ts index cbd9b2033e..84187fe2f7 100644 --- a/tests/unit/image-model-not-in-chat-catalog-6457.test.ts +++ b/tests/unit/image-model-not-in-chat-catalog-6457.test.ts @@ -11,10 +11,10 @@ // `type: "image"` by the imageRegistry loop — and catalogDedupe.ts keys on // (id, type, subtype), so the two distinct-`type` entries both survived. // -// Fix: skip a synced model in the chat-catalog loop when it is already a registered -// image model for that exact provider (open-sse/config/imageRegistry.ts -// isRegisteredImageModel()) — the imageRegistry loop still adds the correctly-typed -// `type: "image"` entry. +// Fix: skip an exact-provider registered image model from the chat-catalog loop only +// when synced metadata does not explicitly advertise `chat` or `responses`. The image +// registry loop still adds the correctly typed image entry, while multi-capability +// models keep both entries. import test from "node:test"; import assert from "node:assert/strict"; @@ -38,6 +38,7 @@ async function resetStorage() { } test.beforeEach(async () => { + v1ModelsCatalog.__resetCatalogBuilderRunsForTest(); await resetStorage(); }); @@ -46,19 +47,19 @@ test.after(async () => { fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); }); -async function seedHuggingFaceConnection() { +async function seedProviderConnection(provider: string) { return providersDb.createProviderConnection({ - provider: "huggingface", + provider, authType: "apikey", - name: `huggingface-${Math.random().toString(16).slice(2, 8)}`, - apiKey: "hf-key", + name: `${provider}-${Math.random().toString(16).slice(2, 8)}`, + apiKey: `${provider}-key`, isActive: true, testStatus: "active", }); } test("#6457 image/diffusion model discovered via live sync is NOT listed as a chat model", async () => { - const connection = await seedHuggingFaceConnection(); + const connection = await seedProviderConnection("huggingface"); // Simulate what HuggingFace's live `/v1/models` discovery persists for an // image/diffusion model: no supportedEndpoints/modality info at all — the exact @@ -100,3 +101,36 @@ test("#6457 image/diffusion model discovered via live sync is NOT listed as a ch assert.equal(entry.type, undefined, "the real chat model must not carry a non-chat type"); } }); + +test("registered image model with explicit chat endpoints keeps both catalog entries", async () => { + const connection = await seedProviderConnection("codex"); + + await modelsDb.replaceSyncedAvailableModelsForConnection("codex", connection.id, [ + { + id: "gpt-5.6-sol", + name: "GPT 5.6 Sol", + supportedEndpoints: ["responses"], + }, + ]); + + const response = await v1ModelsCatalog.getUnifiedModelsResponse( + new Request("http://localhost/api/v1/models?prefix=alias") + ); + assert.equal(response.status, 200); + + const body = (await response.json()) as { + data: Array<{ id: string; type?: string; supported_endpoints?: string[] }>; + }; + const entries = body.data.filter((model) => model.id.endsWith("/gpt-5.6-sol")); + + assert.ok( + entries.some( + (model) => model.type !== "image" && model.supported_endpoints?.includes("responses") + ), + "explicit responses support must keep the synced chat entry" + ); + assert.ok( + entries.some((model) => model.type === "image"), + "the registered image entry must remain available under the same model id" + ); +}); diff --git a/tests/unit/issue-7071-ollama-session-quota.test.ts b/tests/unit/issue-7071-ollama-session-quota.test.ts new file mode 100644 index 0000000000..25b297c0ad --- /dev/null +++ b/tests/unit/issue-7071-ollama-session-quota.test.ts @@ -0,0 +1,103 @@ +/** + * Issue #7071 — Ollama Cloud's 5-hour "session" usage-limit 429 is never + * recognized as quota-exhausted. The upstream returns a body like: + * "you () have reached your session usage limit" + * + * This exactly mirrors the already-fixed "weekly usage limit" gap (#3709, + * #6638): ollama-cloud is an apikey-category provider (not oauth), so the + * oauth-only `shouldUseQuotaSignal` gate in checkFallbackError skips the + * generic subscription-quota-text branch (#2321) for its 429s. Without a + * dedicated, ungated session check the account fell through to the generic + * 429 backoff (~3s, capped low) and got retried within the same 5-hour + * session window instead of cooling down for the session's duration — + * combo/LKGP routing cycled back to the "exhausted" account instead of + * advancing to the next one. + * + * This test proves: (1) the session-usage-limit text is classified as + * QUOTA_EXHAUSTED with a cooldown far longer than the generic backoff cap, + * for BOTH apikey and oauth provider categories, and (2) unrelated + * session-expired/auth wording and the sibling weekly-quota text are + * unaffected. + */ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { checkFallbackError } = await import("../../open-sse/services/accountFallback.ts"); +const { isSessionUsageLimitText, buildSessionQuotaFallback, isWeeklyUsageLimitText } = + await import("../../open-sse/services/quotaTextCooldowns.ts"); +const { RateLimitReason, BACKOFF_CONFIG } = await import("../../open-sse/config/constants.ts"); +const { BACKOFF_CONFIG: ERROR_BACKOFF_CONFIG } = await import("../../open-sse/config/errorConfig.ts"); + +const SESSION_BODY = "you (acme-corp) have reached your session usage limit"; +const SESSION_COOLDOWN_MS = 5 * 60 * 60 * 1000; // 5 hours + +test("#7071 sanity: weekly text IS recognized (already fixed by #3709/#6638)", () => { + assert.equal(isWeeklyUsageLimitText("you (acme-corp) have reached your weekly usage limit"), true); +}); + +test("#7071 isSessionUsageLimitText matches the ollama-cloud 429 body", () => { + assert.equal(isSessionUsageLimitText(SESSION_BODY.toLowerCase()), true); + assert.equal(isSessionUsageLimitText("session limit reached, try later"), true); + assert.equal(isSessionUsageLimitText("rate_limit_exceeded: too many requests"), false); + // Must not false-positive on unrelated "session expired" auth errors. + assert.equal(isSessionUsageLimitText("your session has expired, please log in again"), false); + assert.equal(isSessionUsageLimitText("session token invalid"), false); +}); + +test("#7071 buildSessionQuotaFallback returns a 5h QUOTA_EXHAUSTED cooldown, far above the generic backoff cap", () => { + const result = buildSessionQuotaFallback(SESSION_BODY); + assert.ok(result, "expected a non-null fallback for session-usage-limit text"); + assert.equal(result!.reason, RateLimitReason.QUOTA_EXHAUSTED); + assert.equal(result!.cooldownMs, SESSION_COOLDOWN_MS); + assert.ok(result!.cooldownMs > (ERROR_BACKOFF_CONFIG.max ?? BACKOFF_CONFIG.max)); +}); + +test("#7071 buildSessionQuotaFallback returns null for unrelated error text", () => { + assert.equal(buildSessionQuotaFallback("rate_limit_exceeded: too many requests"), null); + assert.equal(buildSessionQuotaFallback("your session has expired, please log in again"), null); +}); + +test("#7071 BUG: checkFallbackError misclassifies ollama-cloud session-quota 429 as generic RATE_LIMIT_EXCEEDED instead of QUOTA_EXHAUSTED", () => { + const out = checkFallbackError( + 429, + SESSION_BODY, + 0, // backoffLevel + null, // model + "ollama-cloud", // provider (apikey category) + null, // headers + null, // profileOverride + null // structuredError + ); + + assert.equal(out.shouldFallback, true); + assert.equal( + out.reason, + RateLimitReason.QUOTA_EXHAUSTED, + `expected QUOTA_EXHAUSTED for session-usage-limit text, got reason=${out.reason} cooldownMs=${out.cooldownMs}` + ); + assert.equal(out.cooldownMs, SESSION_COOLDOWN_MS); +}); + +test("#7071 checkFallbackError: oauth-category provider with session-limit text also gets the long cooldown", () => { + const out = checkFallbackError(429, SESSION_BODY, 0, null, "claude", null, null, null); + assert.equal(out.reason, RateLimitReason.QUOTA_EXHAUSTED); + assert.equal(out.cooldownMs, SESSION_COOLDOWN_MS); +}); + +test("#7071 checkFallbackError: ollama-cloud generic rate-limit body is unaffected (no false positive)", () => { + const out = checkFallbackError( + 429, + "rate_limit_exceeded: too many requests", + 0, + null, + "ollama-cloud", + null, + null, + null + ); + assert.equal(out.reason, RateLimitReason.RATE_LIMIT_EXCEEDED); + assert.ok( + out.cooldownMs <= 2 * 60 * 1000, + "generic rate limit text must keep the normal short backoff, not the 5h session cooldown" + ); +}); diff --git a/tests/unit/listCapabilities-a2a.test.ts b/tests/unit/listCapabilities-a2a.test.ts index a51357d712..60e636b163 100644 --- a/tests/unit/listCapabilities-a2a.test.ts +++ b/tests/unit/listCapabilities-a2a.test.ts @@ -3,7 +3,7 @@ * * Verifies: * - Return shape matches §3.7 contract - * - Markdown table contains all 43 skill IDs + * - Markdown table contains all 44 skill IDs * - Coverage bounds are within declared totals * - metadata.source === "agent-skills-catalog" * - metadata.generatedAt is an ISO datetime string @@ -31,7 +31,7 @@ test("executeListCapabilities returns shape matching §3.7 contract", async () = const { metadata } = result; assert.ok(metadata, "metadata exists"); assert.equal(metadata.source, "agent-skills-catalog", "metadata.source matches"); - assert.equal(metadata.totalSkills, 44, "metadata.totalSkills === 44 (43 + config)"); + assert.equal(metadata.totalSkills, 45, "metadata.totalSkills === 45 (44 + config)"); assert.ok(metadata.coverage, "metadata.coverage exists"); assert.ok(metadata.coverage.api, "metadata.coverage.api exists"); assert.ok(metadata.coverage.cli, "metadata.coverage.cli exists"); @@ -39,12 +39,12 @@ test("executeListCapabilities returns shape matching §3.7 contract", async () = assert.equal(metadata.coverage.cli.total, 20, "cli.total === 20"); }); -test("executeListCapabilities markdown table contains all 43 API+CLI skill IDs", async () => { +test("executeListCapabilities markdown table contains all 44 API+CLI skill IDs", async () => { const result = await executeListCapabilities(stubTask); const content = result.artifacts[0].content; const allIds = [...API_SKILL_IDS, ...CLI_SKILL_IDS] as string[]; - assert.equal(allIds.length, 43, "API+CLI catalog declares 43 skill IDs"); + assert.equal(allIds.length, 44, "API+CLI catalog declares 44 skill IDs"); for (const id of allIds) { assert.ok(content.includes(id), `Markdown table missing skill ID: ${id}`); @@ -58,11 +58,11 @@ test("metadata.coverage.api.have is within [0, 23]", async () => { assert.ok(api.have <= 23, "api.have <= 23"); }); -test("metadata.coverage.cli.have is within [0, 20]", async () => { +test("metadata.coverage.cli.have is within [0, 21]", async () => { const result = await executeListCapabilities(stubTask); const { cli } = result.metadata.coverage; assert.ok(cli.have >= 0, "cli.have >= 0"); - assert.ok(cli.have <= 20, "cli.have <= 20"); + assert.ok(cli.have <= 21, "cli.have <= 21"); }); test("metadata.generatedAt is a valid ISO datetime", async () => { diff --git a/tests/unit/main-server-keepalive-timeout-7003.test.ts b/tests/unit/main-server-keepalive-timeout-7003.test.ts new file mode 100644 index 0000000000..fd20f19774 --- /dev/null +++ b/tests/unit/main-server-keepalive-timeout-7003.test.ts @@ -0,0 +1,202 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import http from "node:http"; +import net from "node:net"; +import { getMainServerTimeoutConfig } from "../../src/shared/utils/runtimeTimeouts.ts"; + +// #7003 — JetBrains AI Assistant ("Test Connection" / completions) reported +// "HTTP/1.1 header parser received no bytes". The main OmniRoute server +// (scripts/dev/run-next.mjs) boots a bare `http.createServer(...)` and never +// configures `keepAliveTimeout`/`headersTimeout`, leaving Node's http.Server +// default of keepAliveTimeout=5_000ms with no `Keep-Alive: timeout=N` response +// hint. JetBrains AI Assistant's JVM `java.net.http.HttpClient` connection pool +// can reuse a socket idle for longer than that window; the server has already +// torn the socket down, so the client gets 0 response bytes back instead of a +// fresh HTTP response. +// +// This spec proves both halves: +// 1. `getMainServerTimeoutConfig()` raises the defaults well above Node's +// unconfigured 5_000ms window (the actual fix wired into run-next.mjs). +// 2. A bare http.Server left at Node's defaults drops a socket reused after +// an idle gap past 5s, while the same server configured via +// `getMainServerTimeoutConfig()` keeps serving the reused connection. + +describe("#7003 getMainServerTimeoutConfig", () => { + it("defaults keepAliveTimeout/headersTimeout well above Node's 5_000ms default", () => { + const config = getMainServerTimeoutConfig({}); + assert.equal(config.keepAliveTimeoutMs, 65_000); + assert.equal(config.headersTimeoutMs, 66_000); + assert.ok(config.keepAliveTimeoutMs > 5_000, "must exceed Node's unconfigured default"); + assert.ok( + config.headersTimeoutMs > config.keepAliveTimeoutMs, + "headersTimeout must stay above keepAliveTimeout per Node's own requirement" + ); + }); + + it("honors env overrides and keeps headersTimeout coherent with a raised keepAliveTimeout", () => { + const config = getMainServerTimeoutConfig({ + MAIN_SERVER_KEEPALIVE_TIMEOUT_MS: "120000", + MAIN_SERVER_HEADERS_TIMEOUT_MS: "121000", + }); + assert.equal(config.keepAliveTimeoutMs, 120_000); + assert.equal(config.headersTimeoutMs, 121_000); + }); + + it("bumps an inconsistent explicit headersTimeout override above keepAliveTimeout", () => { + const config = getMainServerTimeoutConfig({ + MAIN_SERVER_KEEPALIVE_TIMEOUT_MS: "120000", + MAIN_SERVER_HEADERS_TIMEOUT_MS: "1000", + }); + assert.equal(config.keepAliveTimeoutMs, 120_000); + assert.equal(config.headersTimeoutMs, 121_000); + }); + + it("falls back to defaults on invalid env values", () => { + const config = getMainServerTimeoutConfig({ + MAIN_SERVER_KEEPALIVE_TIMEOUT_MS: "not-a-number", + }); + assert.equal(config.keepAliveTimeoutMs, 65_000); + }); +}); + +/** + * Sends a raw HTTP/1.1 GET over an already-connected keep-alive socket and + * resolves with whatever bytes arrive within a short settle window (empty + * string if nothing comes back — the exact "0 bytes back" failure mode + * JetBrains AI Assistant surfaces as "header parser received no bytes"). + * + * The socket is opened with `allowHalfOpen: true` so it faithfully mimics a + * JVM/OkHttp-style client: Node's default `allowHalfOpen: false` proactively + * ends the writable side the instant it processes an incoming FIN, turning + * the reused write into a synchronous "socket has been ended" error instead + * of the real-world race — a write that is accepted locally (the server + * already destroyed the connection, so it never arrives) whose response + * settles as 0 bytes. + */ +function sendKeepAliveRequest(socket: net.Socket, port: number): Promise { + return new Promise((resolve) => { + let received = ""; + let settleTimer: NodeJS.Timeout; + const finish = () => { + socket.off("data", onData); + clearTimeout(settleTimer); + resolve(received); + }; + // A short settle window once the full chunked response has arrived (fast + // path); a generous cap in case nothing ever comes back — the torn-down + // connection case this test proves, and a safety margin against first-run + // JIT/module-load jitter under the test runner. + const onData = (chunk: Buffer) => { + received += chunk.toString("utf8"); + if (received.endsWith("0\r\n\r\n")) { + clearTimeout(settleTimer); + settleTimer = setTimeout(finish, 50); + } + }; + socket.on("data", onData); + socket.write(`GET / HTTP/1.1\r\nHost: 127.0.0.1:${port}\r\nConnection: keep-alive\r\n\r\n`); + settleTimer = setTimeout(finish, 3_000); + }); +} + +function startEchoServer(configure: (server: http.Server) => void): Promise { + return new Promise((resolve) => { + const server = http.createServer((_req, res) => { + res.writeHead(200, { "content-type": "text/plain" }); + res.end("ok"); + }); + configure(server); + server.listen(0, "127.0.0.1", () => resolve(server)); + }); +} + +async function withServer( + configure: (server: http.Server) => void, + run: (port: number) => Promise +): Promise { + const server = await startEchoServer(configure); + try { + const address = server.address(); + if (typeof address !== "object" || address === null) { + throw new Error("expected server to bind a TCP address"); + } + await run(address.port); + } finally { + await new Promise((resolve) => server.close(() => resolve())); + } +} + +// Node's default keepAliveTimeout is 5_000ms, but the server only starts that +// timer once the response has fully flushed and there is a small amount of +// internal scheduling overhead before the socket is actually torn down — +// empirically ~5.8-6s end-to-end on loopback. 6.5s reliably clears that +// window without relying on a hair-trigger race. +const IDLE_GAP_MS = 6_500; + +describe("#7003 keep-alive socket reuse across an idle gap", () => { + it( + "current Node defaults (keepAliveTimeout=5000ms): a pooled socket reused after 6.5s idle gets 0 bytes back", + { timeout: 30_000 }, + async () => { + await withServer( + () => { + /* leave Node's http.Server defaults untouched (keepAliveTimeout=5000ms) */ + }, + async (port) => { + const socket = net.connect({ port, host: "127.0.0.1", allowHalfOpen: true }); + await new Promise((resolve, reject) => { + socket.once("connect", () => resolve()); + socket.once("error", reject); + }); + + const first = await sendKeepAliveRequest(socket, port); + assert.match(first, /200/, "first request on a fresh socket must succeed"); + + await new Promise((resolve) => setTimeout(resolve, IDLE_GAP_MS)); + + const second = await sendKeepAliveRequest(socket, port); + assert.equal( + second, + "", + "reusing the idle-torn-down socket must get exactly 0 bytes back (the reported bug)" + ); + socket.destroy(); + } + ); + } + ); + + it( + "fixed config (getMainServerTimeoutConfig): the same reused connection stays alive past 6.5s idle", + { timeout: 30_000 }, + async () => { + const fixedTimeouts = getMainServerTimeoutConfig({}); + await withServer( + (server) => { + server.keepAliveTimeout = fixedTimeouts.keepAliveTimeoutMs; + server.headersTimeout = fixedTimeouts.headersTimeoutMs; + }, + async (port) => { + const socket = net.connect({ port, host: "127.0.0.1", allowHalfOpen: true }); + await new Promise((resolve, reject) => { + socket.once("connect", () => resolve()); + socket.once("error", reject); + }); + + const first = await sendKeepAliveRequest(socket, port); + assert.match(first, /200/, "first request on a fresh socket must succeed"); + + await new Promise((resolve) => setTimeout(resolve, IDLE_GAP_MS)); + + const second = await sendKeepAliveRequest(socket, port); + assert.match( + second, + /200/, + "the reused connection must still get a valid response after the fix" + ); + socket.destroy(); + } + ); + } + ); +}); diff --git a/tests/unit/main-server-timeouts-parity.test.ts b/tests/unit/main-server-timeouts-parity.test.ts new file mode 100644 index 0000000000..7a8374c942 --- /dev/null +++ b/tests/unit/main-server-timeouts-parity.test.ts @@ -0,0 +1,39 @@ +import test from "node:test"; +import assert from "node:assert"; +import { getMainServerTimeoutConfig as mjsImpl } from "../../scripts/dev/main-server-timeouts.mjs"; +import { getMainServerTimeoutConfig as tsImpl } from "../../src/shared/utils/runtimeTimeouts.ts"; + +// The shipped server-ws.mjs uses the SIBLING scripts/dev/main-server-timeouts.mjs +// (a ../../src import escapes the package after the dist copy — 2026-07-15 boot +// crash, #7065 class). This parity matrix is the anti-drift guard between the +// sibling and the canonical src/shared/utils/runtimeTimeouts.ts implementation. +const ENV_MATRIX: Record[] = [ + {}, + { MAIN_SERVER_KEEPALIVE_TIMEOUT_MS: "70000" }, + { MAIN_SERVER_HEADERS_TIMEOUT_MS: "80000" }, + { MAIN_SERVER_KEEPALIVE_TIMEOUT_MS: "90000", MAIN_SERVER_HEADERS_TIMEOUT_MS: "10000" }, + { MAIN_SERVER_KEEPALIVE_TIMEOUT_MS: "0", MAIN_SERVER_HEADERS_TIMEOUT_MS: "0" }, + { MAIN_SERVER_KEEPALIVE_TIMEOUT_MS: "abc" }, + { MAIN_SERVER_KEEPALIVE_TIMEOUT_MS: " " }, + { MAIN_SERVER_KEEPALIVE_TIMEOUT_MS: "-5" }, + { MAIN_SERVER_KEEPALIVE_TIMEOUT_MS: "1234.9" }, +]; + +test("sibling main-server-timeouts.mjs stays in parity with runtimeTimeouts.ts", () => { + for (const env of ENV_MATRIX) { + assert.deepStrictEqual( + mjsImpl(env), + tsImpl(env), + `divergence for env ${JSON.stringify(env)}` + ); + } +}); + +test("invalid values log through the provided logger in both implementations", () => { + const logsA: string[] = []; + const logsB: string[] = []; + mjsImpl({ MAIN_SERVER_KEEPALIVE_TIMEOUT_MS: "bogus" }, (m) => logsA.push(m)); + tsImpl({ MAIN_SERVER_KEEPALIVE_TIMEOUT_MS: "bogus" }, (m) => logsB.push(m)); + assert.strictEqual(logsA.length, 1); + assert.deepStrictEqual(logsA, logsB); +}); diff --git a/tests/unit/materialize-bundled-symlinks.test.ts b/tests/unit/materialize-bundled-symlinks.test.ts new file mode 100644 index 0000000000..a83dc980bd --- /dev/null +++ b/tests/unit/materialize-bundled-symlinks.test.ts @@ -0,0 +1,225 @@ +import assert from "node:assert/strict"; +import { + existsSync, + lstatSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + symlinkSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; +// @ts-expect-error - JS build helper without type declarations +import { + materializeBundledSymlinks, + syncRebuiltNativeModuleIntoHashedEntries, +} from "../../scripts/build/assembleStandalone.mjs"; + +function makePkg(dir: string, name: string, marker: string) { + const pkgDir = join(dir, name); + mkdirSync(pkgDir, { recursive: true }); + writeFileSync(join(pkgDir, "package.json"), JSON.stringify({ name, marker })); + return pkgDir; +} + +test("materializeBundledSymlinks dereferences a live symlink into a real directory", () => { + const root = mkdtempSync(join(tmpdir(), "mbs-live-")); + try { + const realPkgHome = join(root, "external"); + mkdirSync(realPkgHome, { recursive: true }); + makePkg(realPkgHome, "ws", "real-ws"); + + const nm = join(root, "bundle", "node_modules"); + mkdirSync(nm, { recursive: true }); + symlinkSync(join(realPkgHome, "ws"), join(nm, "ws-a972e7ffa40ff725"), "dir"); + + const summary = materializeBundledSymlinks(nm); + + assert.equal(summary.materialized, 1); + const target = join(nm, "ws-a972e7ffa40ff725"); + assert.equal(lstatSync(target).isSymbolicLink(), false); + assert.equal(lstatSync(target).isDirectory(), true); + assert.equal(JSON.parse(readFileSync(join(target, "package.json"), "utf8")).marker, "real-ws"); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("materializeBundledSymlinks relinks a dangling hashed symlink to its sibling real package", () => { + const root = mkdtempSync(join(tmpdir(), "mbs-dangle-")); + try { + const nm = join(root, "node_modules"); + mkdirSync(nm, { recursive: true }); + // Sibling real package (as copied by copyNativeAssetsAndExtraModules). + makePkg(nm, "better-sqlite3", "real-bsq"); + // Dangling absolute link into a build machine that does not exist here. + symlinkSync( + "/Users/runner/work/OmniRoute/OmniRoute/.build/next/standalone/node_modules/better-sqlite3", + join(nm, "better-sqlite3-90e2652d1716b047"), + "dir" + ); + + const summary = materializeBundledSymlinks(nm); + + assert.equal(summary.relinked, 1); + const target = join(nm, "better-sqlite3-90e2652d1716b047"); + assert.equal(lstatSync(target).isSymbolicLink(), false); + assert.equal(JSON.parse(readFileSync(join(target, "package.json"), "utf8")).marker, "real-bsq"); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("materializeBundledSymlinks drops a dangling link with no resolvable sibling", () => { + const root = mkdtempSync(join(tmpdir(), "mbs-drop-")); + try { + const nm = join(root, "node_modules"); + mkdirSync(nm, { recursive: true }); + symlinkSync( + "/nonexistent/build/machine/path/mystery", + join(nm, "mystery-deadbeefcafe0001"), + "dir" + ); + + const summary = materializeBundledSymlinks(nm); + + assert.equal(summary.removed, 1); + assert.equal(existsSync(join(nm, "mystery-deadbeefcafe0001")), false); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("materializeBundledSymlinks handles scoped-package symlinks", () => { + const root = mkdtempSync(join(tmpdir(), "mbs-scope-")); + try { + const realPkgHome = join(root, "external"); + mkdirSync(join(realPkgHome, "@huggingface"), { recursive: true }); + makePkg(join(realPkgHome, "@huggingface"), "transformers", "real-hf"); + + const nm = join(root, "node_modules"); + mkdirSync(join(nm, "@huggingface"), { recursive: true }); + symlinkSync( + join(realPkgHome, "@huggingface", "transformers"), + join(nm, "@huggingface", "transformers-abc1234567890def"), + "dir" + ); + + const summary = materializeBundledSymlinks(nm); + + assert.equal(summary.materialized, 1); + const target = join(nm, "@huggingface", "transformers-abc1234567890def"); + assert.equal(lstatSync(target).isSymbolicLink(), false); + assert.equal(JSON.parse(readFileSync(join(target, "package.json"), "utf8")).marker, "real-hf"); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("materializeBundledSymlinks leaves real directories untouched and no-ops on missing dir", () => { + const root = mkdtempSync(join(tmpdir(), "mbs-noop-")); + try { + const nm = join(root, "node_modules"); + mkdirSync(nm, { recursive: true }); + makePkg(nm, "pino", "real-pino"); + + const summary = materializeBundledSymlinks(nm); + assert.deepEqual(summary, { materialized: 0, relinked: 0, removed: 0 }); + assert.equal(existsSync(join(nm, "pino", "package.json")), true); + + const missing = materializeBundledSymlinks(join(root, "does-not-exist")); + assert.deepEqual(missing, { materialized: 0, relinked: 0, removed: 0 }); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("syncRebuiltNativeModuleIntoHashedEntries overwrites a hashed entry with the rebuilt root module", () => { + const root = mkdtempSync(join(tmpdir(), "sync-hashed-")); + try { + const rootModule = makePkg(root, "better-sqlite3", "electron-abi-rebuilt"); + + const nm = join(root, "nested", "node_modules"); + makePkg(nm, "better-sqlite3-90e2652d1716b047", "stale-node-abi"); + + const summary = syncRebuiltNativeModuleIntoHashedEntries(rootModule, nm); + + assert.equal(summary.synced, 1); + const target = join(nm, "better-sqlite3-90e2652d1716b047"); + assert.equal( + JSON.parse(readFileSync(join(target, "package.json"), "utf8")).marker, + "electron-abi-rebuilt" + ); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("syncRebuiltNativeModuleIntoHashedEntries overwrites a plain-named entry too", () => { + const root = mkdtempSync(join(tmpdir(), "sync-plain-")); + try { + const rootModule = makePkg(root, "better-sqlite3", "electron-abi-rebuilt"); + + const nm = join(root, "nested", "node_modules"); + makePkg(nm, "better-sqlite3", "stale-node-abi"); + + const summary = syncRebuiltNativeModuleIntoHashedEntries(rootModule, nm); + + assert.equal(summary.synced, 1); + assert.equal( + JSON.parse(readFileSync(join(nm, "better-sqlite3", "package.json"), "utf8")).marker, + "electron-abi-rebuilt" + ); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("syncRebuiltNativeModuleIntoHashedEntries no-ops when root module or nested node_modules is missing", () => { + const root = mkdtempSync(join(tmpdir(), "sync-noop-")); + try { + const rootModule = join(root, "does-not-exist", "better-sqlite3"); + const nm = join(root, "nested", "node_modules"); + makePkg(nm, "better-sqlite3", "stale-node-abi"); + + const missingRoot = syncRebuiltNativeModuleIntoHashedEntries(rootModule, nm); + assert.deepEqual(missingRoot, { synced: 0 }); + + const realRootModule = makePkg(root, "better-sqlite3", "electron-abi-rebuilt"); + const missingNm = syncRebuiltNativeModuleIntoHashedEntries( + realRootModule, + join(root, "does-not-exist-nm") + ); + assert.deepEqual(missingNm, { synced: 0 }); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("syncRebuiltNativeModuleIntoHashedEntries leaves unrelated entries untouched", () => { + const root = mkdtempSync(join(tmpdir(), "sync-unrelated-")); + try { + const rootModule = makePkg(root, "better-sqlite3", "electron-abi-rebuilt"); + + const nm = join(root, "nested", "node_modules"); + makePkg(nm, "pino", "real-pino"); + makePkg(nm, "better-sqlite3-helper", "unrelated-package"); + + const summary = syncRebuiltNativeModuleIntoHashedEntries(rootModule, nm); + + assert.deepEqual(summary, { synced: 0 }); + assert.equal( + JSON.parse(readFileSync(join(nm, "pino", "package.json"), "utf8")).marker, + "real-pino" + ); + assert.equal( + JSON.parse(readFileSync(join(nm, "better-sqlite3-helper", "package.json"), "utf8")).marker, + "unrelated-package" + ); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/tests/unit/minimax-m3-model-registry.test.ts b/tests/unit/minimax-m3-model-registry.test.ts index 820cbc77ca..2aae2b3a69 100644 --- a/tests/unit/minimax-m3-model-registry.test.ts +++ b/tests/unit/minimax-m3-model-registry.test.ts @@ -29,13 +29,11 @@ describe("MiniMax M3 model registration (#3110)", () => { assert.equal(m3.contextLength, 1_048_576); }); - it("opencode provider has minimax-m3-free with 1M context", () => { + it("opencode provider does NOT list minimax-m3-free (#6998 — delisted upstream, 401)", () => { const entry = REGISTRY.opencode; assert.ok(entry, "opencode registry entry must exist"); const m3 = entry.models.find((m) => m.id === "minimax-m3-free"); - assert.ok(m3, "minimax-m3-free must be in opencode models"); - assert.equal(m3.name, "MiniMax M3 Free"); - assert.equal(m3.contextLength, 1_048_576); + assert.equal(m3, undefined, "minimax-m3-free was delisted from OpenCode Zen's free tier (#6998)"); }); it("opencode-go provider has minimax-m3 with Claude targetFormat", () => { diff --git a/tests/unit/mitm-stop-dns-before-kill-1809.test.ts b/tests/unit/mitm-stop-dns-before-kill-1809.test.ts new file mode 100644 index 0000000000..3dd92dcaad --- /dev/null +++ b/tests/unit/mitm-stop-dns-before-kill-1809.test.ts @@ -0,0 +1,82 @@ +/** + * Regression test for upstream issue #1809: "connect ECONNREFUSED 127.0.0.1:443" + * after stopping the MITM proxy. + * + * Root cause: stopMitm() killed the spawned MITM server process FIRST, and only + * removed the /etc/hosts DNS-spoof entries AFTER. During that window any client + * whose DNS still resolved the target host to 127.0.0.1 (from startMitm's spoof) + * but whose MITM listener was already dead got ECONNREFUSED — exactly the + * community-confirmed workaround ("stop DNS before stopping the server") proves. + * + * This test drives stopMitm() with real DI: a fake serverProcess standing in for + * the spawned MITM child, and dependency-injected DNS-removal functions that + * record the order in which they are invoked relative to the process kill. The + * fix must remove DNS entries before killing the server process so no window + * exists where DNS points at 127.0.0.1 with nothing listening there. + * + * Uses the project's DATA_DIR-tmp + resetDbInstance pattern so the Node native + * test runner does not hang on open SQLite handles (CLAUDE.md PII learning #3). + */ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { EventEmitter } from "node:events"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-mitm-stop-order-")); +process.env.DATA_DIR = TEST_DATA_DIR; + +const core = await import("../../src/lib/db/core.ts"); +const manager = await import("../../src/mitm/manager.ts"); + +test.after(() => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +test("stopMitm removes DNS entries before killing the MITM server process (#1809)", async () => { + const events: string[] = []; + + // Fake child process standing in for the spawned MITM server. + const fakeProc = new EventEmitter() as EventEmitter & { + killed: boolean; + kill: (signal?: string) => boolean; + }; + fakeProc.killed = false; + fakeProc.kill = (signal?: string) => { + events.push(`kill:${signal}`); + fakeProc.killed = true; + return true; + }; + + manager.__setServerProcessForTest(fakeProc as unknown as import("child_process").ChildProcess, 4242); + + const removeDNSEntry = async () => { + events.push("removeDNSEntry"); + }; + const removeDNSEntries = async () => { + events.push("removeDNSEntries"); + }; + const collectManagedHosts = () => ["fake.example.test"]; + + await manager.stopMitm("fake-sudo-password", { + removeDNSEntry, + removeDNSEntries, + collectManagedHosts, + }); + + const firstKillIndex = events.findIndex((e) => e.startsWith("kill:")); + const firstDnsIndex = events.findIndex( + (e) => e === "removeDNSEntry" || e === "removeDNSEntries" + ); + + assert.ok(firstKillIndex !== -1, "server process kill was never invoked"); + assert.ok(firstDnsIndex !== -1, "DNS removal was never invoked"); + assert.ok( + firstDnsIndex < firstKillIndex, + `DNS entries must be removed BEFORE the MITM server process is killed ` + + `(got order: ${JSON.stringify(events)}) — otherwise a client whose DNS still ` + + `points at 127.0.0.1 hits a dead listener and gets ECONNREFUSED (#1809)` + ); +}); diff --git a/tests/unit/next-config.test.ts b/tests/unit/next-config.test.ts index 29821fa4ec..9281175217 100644 --- a/tests/unit/next-config.test.ts +++ b/tests/unit/next-config.test.ts @@ -288,6 +288,30 @@ test("turbopack.ignoreIssue suppresses the agentSkills over-bundling warning (#6 assert.match(String(agentSkillsRule.description), /Overly broad patterns/); }); +test("turbopack.ignoreIssue suppresses the compression module over-bundling warning (#7051)", async () => { + // open-sse/services/compression/ruleLoader.ts and + // .../engines/rtk/filterLoader.ts both define an identical getModuleDir() + // helper that walks up directories via path.resolve(anchor) + + // fs.existsSync(...) in a loop with a non-literal argument — the same + // class of dynamic-path fs access that #6582 suppressed for + // src/lib/agentSkills/**, but that narrow allowlist glob didn't cover this + // module, so the warning kept firing (610 times) for every entry point + // transitively importing the compression module. This guards the config + // shape so the suppression rule isn't silently dropped in a future edit. + const { default: nextConfig } = await loadNextConfig("ignore-issue-compression"); + const rules = nextConfig.turbopack?.ignoreIssue; + + assert.ok(Array.isArray(rules), "expected turbopack.ignoreIssue to be an array"); + const compressionRule = rules.find((rule) => + String(rule.path).includes("open-sse/services/compression") + ); + assert.ok( + compressionRule, + "expected an ignoreIssue rule targeting open-sse/services/compression/**" + ); + assert.match(String(compressionRule.description), /Overly broad patterns/); +}); + test("optimizePackageImports excludes the internal @omniroute/open-sse workspace (build-OOM guard)", async () => { // Regression guard: adding the internal `@omniroute/open-sse` workspace to // optimizePackageImports makes Next.js resolve its entire barrel at build diff --git a/tests/unit/noAuthProxyResolution.relayAuth.test.ts b/tests/unit/noAuthProxyResolution.relayAuth.test.ts new file mode 100644 index 0000000000..f838038a0f --- /dev/null +++ b/tests/unit/noAuthProxyResolution.relayAuth.test.ts @@ -0,0 +1,49 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { resolveAccountProxies } from "../../src/sse/services/noAuthProxyResolution.ts"; + +test("resolveAccountProxies preserves relayAuth for relay-type (vercel/deno/cloudflare) pool proxies", async () => { + const fakeVercelProxyRow = { + id: "proxy-1", + type: "vercel", + host: "my-relay-abc123.vercel.app", + port: 443, + username: null, + password: null, + notes: JSON.stringify({ relayAuth: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" }), + }; + + const resolved = await resolveAccountProxies( + [{ fingerprint: "acct-1", proxyId: "proxy-1" }], + async (id) => (id === "proxy-1" ? fakeVercelProxyRow : null) + ); + + const proxy = resolved[0].proxy as unknown as { type?: string; relayAuth?: string }; + assert.equal(proxy?.type, "vercel"); + assert.equal( + proxy?.relayAuth, + "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + "relayAuth must survive resolveAccountProxies() for relay-type (vercel/deno/cloudflare) proxies" + ); +}); + +test("resolveAccountProxies leaves relayAuth absent for plain non-relay (socks5/http) pool proxies", async () => { + const fakeSocksProxyRow = { + id: "proxy-2", + type: "socks5", + host: "1.2.3.4", + port: 1080, + username: "u", + password: "p", + notes: JSON.stringify({ relayAuth: "should-not-leak-onto-non-relay-types" }), + }; + + const resolved = await resolveAccountProxies( + [{ fingerprint: "acct-2", proxyId: "proxy-2" }], + async (id) => (id === "proxy-2" ? fakeSocksProxyRow : null) + ); + + const proxy = resolved[0].proxy as unknown as { type?: string; relayAuth?: string }; + assert.equal(proxy?.type, "socks5"); + assert.equal(proxy?.relayAuth, undefined); +}); diff --git a/tests/unit/ollama-cloud-usage.test.ts b/tests/unit/ollama-cloud-usage.test.ts index 9c54789911..2037426daa 100644 --- a/tests/unit/ollama-cloud-usage.test.ts +++ b/tests/unit/ollama-cloud-usage.test.ts @@ -11,6 +11,28 @@ test("USAGE_SUPPORTED_PROVIDERS includes ollama-cloud", () => { ); }); +test("USAGE_FETCHER_PROVIDERS includes ollama-cloud (#7026)", () => { + // getUsageForProvider's switch handles `case "ollama-cloud"`, and the array's doc comment + // requires it to stay in sync with that switch. If it drifts, registerGenericQuotaFetchers + // never registers a preflight quota fetcher for ollama-cloud even though the scraper exists. + assert.ok( + (usage.USAGE_FETCHER_PROVIDERS as readonly string[]).includes("ollama-cloud"), + "ollama-cloud is handled by getUsageForProvider's switch and must be listed in USAGE_FETCHER_PROVIDERS" + ); +}); + +test("registerGenericQuotaFetchers wires a preflight quota fetcher for ollama-cloud (#7026)", async () => { + const { registerGenericQuotaFetchers } = await import( + "../../open-sse/services/genericQuotaFetcher.ts" + ); + const { getQuotaFetcher } = await import("../../open-sse/services/quotaPreflight.ts"); + registerGenericQuotaFetchers(); + assert.ok( + getQuotaFetcher("ollama-cloud"), + "a generic quota fetcher must be registered for ollama-cloud after registerGenericQuotaFetchers()" + ); +}); + test("getUsageForProvider returns helpful message when Ollama Cloud has no usage cookie", async () => { const originalCookie = process.env.OLLAMA_USAGE_COOKIE; const originalOmniCookie = process.env.OMNIROUTE_OLLAMA_USAGE_COOKIE; diff --git a/tests/unit/openai-to-claude-strip-empty-signature-6953.test.ts b/tests/unit/openai-to-claude-strip-empty-signature-6953.test.ts new file mode 100644 index 0000000000..f129a64f70 --- /dev/null +++ b/tests/unit/openai-to-claude-strip-empty-signature-6953.test.ts @@ -0,0 +1,202 @@ +/** + * TDD regression for #6953 — thinking blocks with empty signatures poison the + * Anthropic leg of combo/blend routes. + * + * Non-Anthropic providers (codex/gpt-5.x) synthesize Anthropic-format `thinking` + * blocks with `signature: ""`. When the client replays these in the next + * request's history, the Anthropic leg rejects them with HTTP 400 "Invalid + * signature in thinking block", and the router silently falls back to codex + * permanently. + * + * The old code fabricated a DEFAULT_THINKING_CLAUDE_SIGNATURE to fill the empty + * signature — but that fabricated signature is equally foreign to Anthropic, so + * it also 400'd. + * + * Fix (#6953): strip thinking blocks with empty/missing signatures entirely. + * They carry no replayable cryptographic value. For `redacted_thinking`, strip + * if `data` is empty/missing for the same reason. + */ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { openaiToClaudeRequest } = + await import("../../open-sse/translator/request/openai-to-claude.ts"); + +test('#6953: thinking block with signature:"" is stripped, not fabricated', () => { + const result = openaiToClaudeRequest( + "claude-opus-4-8", + { + messages: [ + { role: "user", content: "hello" }, + { + role: "assistant", + content: [ + { type: "text", text: "I will help you." }, + { type: "thinking", thinking: "reasoning here", signature: "" }, + { + type: "text", + text: "Let me use a tool.", + }, + ], + }, + { role: "user", content: "ok go ahead" }, + ], + }, + false + ); + + const assistant = result.messages.find((m) => m.role === "assistant"); + assert.ok(assistant, "expected assistant message"); + + // The thinking block with empty signature must be DROPPED, not preserved + // with a fabricated signature. + const thinkingBlocks = assistant.content.filter((b) => b && b.type === "thinking"); + assert.equal( + thinkingBlocks.length, + 0, + "thinking block with empty signature must be stripped, not fabricated" + ); + + // Text blocks must survive + const textBlocks = assistant.content.filter((b) => b && b.type === "text"); + assert.ok(textBlocks.length >= 1, "text blocks must be preserved"); +}); + +test("#6953: thinking block with valid signature is preserved verbatim", () => { + const realSig = "EuY2xhdWRlLXNpZ25hdHVyZS0xNzA5..."; + const result = openaiToClaudeRequest( + "claude-opus-4-8", + { + messages: [ + { role: "user", content: "hello" }, + { + role: "assistant", + content: [ + { type: "thinking", thinking: "real reasoning", signature: realSig }, + { type: "text", text: "response" }, + ], + }, + { role: "user", content: "ok" }, + ], + }, + false + ); + + const assistant = result.messages.find((m) => m.role === "assistant"); + assert.ok(assistant); + + const thinkingBlocks = assistant.content.filter((b) => b && b.type === "thinking"); + assert.equal(thinkingBlocks.length, 1, "valid thinking block must be preserved"); + assert.equal(thinkingBlocks[0].signature, realSig, "valid signature must be preserved verbatim"); +}); + +test("#6953: thinking block with undefined signature (Claude-format) is preserved with fallback", () => { + // Claude-format messages may have thinking blocks without a signature field at all. + // These are legitimate and must NOT be stripped — only signature:"" (empty string) + // indicates a non-Anthropic synthesized block. + const result = openaiToClaudeRequest( + "claude-opus-4-8", + { + messages: [ + { role: "user", content: "hello" }, + { + role: "assistant", + content: [ + { type: "thinking", thinking: "I already have this" }, + { type: "text", text: "response" }, + ], + }, + { role: "user", content: "ok" }, + ], + }, + false + ); + + const assistant = result.messages.find((m) => m.role === "assistant"); + assert.ok(assistant); + + const thinkingBlocks = assistant.content.filter((b) => b && b.type === "thinking"); + assert.equal( + thinkingBlocks.length, + 1, + "thinking block with undefined signature must be preserved" + ); + assert.equal(thinkingBlocks[0].thinking, "I already have this", "thinking content must match"); + assert.ok(thinkingBlocks[0].signature, "fallback signature must be applied"); +}); + +test("#6953: redacted_thinking with empty data is stripped", () => { + const result = openaiToClaudeRequest( + "claude-opus-4-8", + { + messages: [ + { role: "user", content: "hello" }, + { + role: "assistant", + content: [ + { type: "redacted_thinking", data: "" }, + { type: "text", text: "response" }, + ], + }, + { role: "user", content: "ok" }, + ], + }, + false + ); + + const assistant = result.messages.find((m) => m.role === "assistant"); + assert.ok(assistant); + + const redactedBlocks = assistant.content.filter((b) => b && b.type === "redacted_thinking"); + assert.equal(redactedBlocks.length, 0, "redacted_thinking with empty data must be stripped"); +}); + +test("#6953: combo scenario — codex-sourced thinking block does not block Anthropic leg", () => { + // Simulates a combo route: turn 1 served by codex produced a thinking block + // with signature:"". Turn 2 should be able to route to Anthropic without + // the poisoned block causing a 400. + const result = openaiToClaudeRequest( + "claude-opus-4-8", + { + messages: [ + { role: "user", content: "write a function" }, + { + role: "assistant", + content: [ + { + type: "thinking", + thinking: "**Reviewing the request**\n\nI need to write a function...", + signature: "", // codex-sourced, no real signature + }, + { type: "text", text: "Here's the function:" }, + { + type: "tool_use", + id: "toolu_01abc", + name: "write_file", + input: { path: "main.rs", content: "fn main() {}" }, + }, + ], + }, + { role: "user", content: "looks good, now add tests" }, + ], + }, + false + ); + + const assistant = result.messages.find((m) => m.role === "assistant"); + assert.ok(assistant); + + // No thinking block with empty-string signature should survive + const badThinking = assistant.content.find( + (b) => b && b.type === "thinking" && b.signature === "" + ); + assert.equal( + badThinking, + undefined, + "no thinking block with empty-string signature should survive" + ); + + // Tool use must survive + const toolUse = assistant.content.find((b) => b && b.type === "tool_use"); + assert.ok(toolUse, "tool_use block must be preserved"); +}); diff --git a/tests/unit/opencode-free-tier-catalog-stale-6998.test.ts b/tests/unit/opencode-free-tier-catalog-stale-6998.test.ts new file mode 100644 index 0000000000..c1d3eeae09 --- /dev/null +++ b/tests/unit/opencode-free-tier-catalog-stale-6998.test.ts @@ -0,0 +1,40 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { opencodeProvider } = await import( + "../../open-sse/config/providers/registry/opencode/index.ts" +); + +function modelIds(): string[] { + return (opencodeProvider.models ?? []).map((m) => m.id); +} + +const DELISTED_FREE_MODELS = [ + "minimax-m3-free", + "minimax-m2.5-free", + "ling-2.6-1t-free", + "trinity-large-preview-free", + "nemotron-3-super-free", + "qwen3.6-plus-free", +]; + +const LIVE_FREE_MODELS_MISSING_FROM_CATALOG = [ + "mimo-v2.5-free", + "hy3-free", + "nemotron-3-ultra-free", + "north-mini-code-free", +]; + +test("issue #6998: oc registry does not advertise delisted free-tier models", () => { + const ids = modelIds(); + for (const delisted of DELISTED_FREE_MODELS) { + assert.ok(!ids.includes(delisted), `oc registry still advertises delisted upstream model "${delisted}"`); + } +}); + +test("issue #6998: oc registry advertises the current live free-tier models", () => { + const ids = modelIds(); + for (const live of LIVE_FREE_MODELS_MISSING_FROM_CATALOG) { + assert.ok(ids.includes(live), `oc registry is missing live upstream free-tier model "${live}"`); + } +}); diff --git a/tests/unit/opencode-go-quota-no-zai.test.ts b/tests/unit/opencode-go-quota-no-zai.test.ts new file mode 100644 index 0000000000..b4775040fa --- /dev/null +++ b/tests/unit/opencode-go-quota-no-zai.test.ts @@ -0,0 +1,31 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; + +import { getOpenCodeGoUsage } from "../../open-sse/services/opencodeOllamaUsage.ts"; + +test("getOpenCodeGoUsage does not send the user's OpenCode Go API key to api.z.ai by default", async () => { + const originalFetch = globalThis.fetch; + const originalEnv = process.env.OMNIROUTE_OPENCODE_GO_QUOTA_URL; + delete process.env.OMNIROUTE_OPENCODE_GO_QUOTA_URL; + + let calledHost: string | null = null; + globalThis.fetch = (async (input: RequestInfo | URL) => { + const url = typeof input === "string" ? input : input.toString(); + calledHost = new URL(url).host; + throw new Error(`unexpected outbound fetch to ${url}`); + }) as typeof fetch; + + try { + const result = await getOpenCodeGoUsage("sk-fake-opencode-go-key", undefined); + assert.notStrictEqual(calledHost, "api.z.ai"); + assert.strictEqual(calledHost, null); + assert.ok( + typeof result.message === "string" && result.message.length > 0, + "expected a descriptive message when no quota URL is configured" + ); + } finally { + globalThis.fetch = originalFetch; + if (originalEnv === undefined) delete process.env.OMNIROUTE_OPENCODE_GO_QUOTA_URL; + else process.env.OMNIROUTE_OPENCODE_GO_QUOTA_URL = originalEnv; + } +}); diff --git a/tests/unit/opencode-go-usage.test.ts b/tests/unit/opencode-go-usage.test.ts index 64e6357800..302a95f244 100644 --- a/tests/unit/opencode-go-usage.test.ts +++ b/tests/unit/opencode-go-usage.test.ts @@ -1,9 +1,25 @@ -import test from "node:test"; +import test, { after } from "node:test"; import assert from "node:assert/strict"; +// The OpenCode Go quota-by-API-key path is opt-in only (see #7022 — there is no +// working default quota endpoint, so OMNIROUTE_OPENCODE_GO_QUOTA_URL must be set +// explicitly by the operator). The module reads this env var once at import time, +// so it has to be set BEFORE the dynamic import below for the opt-in tests in this +// file (which simulate an operator who configured the URL) to exercise the fetch path. +const ORIGINAL_OPENCODE_GO_QUOTA_URL = process.env.OMNIROUTE_OPENCODE_GO_QUOTA_URL; +process.env.OMNIROUTE_OPENCODE_GO_QUOTA_URL = "https://api.z.ai/api/monitor/usage/quota/limit"; + const usage = await import("../../open-sse/services/usage.ts"); const { USAGE_SUPPORTED_PROVIDERS } = await import("../../src/shared/constants/providers.ts"); +after(() => { + if (ORIGINAL_OPENCODE_GO_QUOTA_URL === undefined) { + delete process.env.OMNIROUTE_OPENCODE_GO_QUOTA_URL; + } else { + process.env.OMNIROUTE_OPENCODE_GO_QUOTA_URL = ORIGINAL_OPENCODE_GO_QUOTA_URL; + } +}); + test("USAGE_SUPPORTED_PROVIDERS includes opencode-go", () => { assert.ok( (USAGE_SUPPORTED_PROVIDERS as string[]).includes("opencode-go"), @@ -298,7 +314,8 @@ test("getUsageForProvider returns message for invalid OpenCode Go API keys", asy })) as { message: string }; assert.equal( result.message, - "OpenCode Go API key is valid for chat/models but cannot read quota from the Z.AI quota API. " + + "OpenCode Go API key is valid for chat/models but cannot read quota from the configured " + + "OMNIROUTE_OPENCODE_GO_QUOTA_URL endpoint. " + "Set OPENCODE_GO_WORKSPACE_ID and OPENCODE_GO_AUTH_COOKIE to enable dashboard quota scraping." ); } finally { @@ -342,7 +359,8 @@ test("getUsageForProvider returns message when OpenCode Go quota API returns 200 })) as { message: string }; assert.equal( result.message, - "OpenCode Go API key is valid for chat/models but cannot read quota from the Z.AI quota API. " + + "OpenCode Go API key is valid for chat/models but cannot read quota from the configured " + + "OMNIROUTE_OPENCODE_GO_QUOTA_URL endpoint. " + "Set OPENCODE_GO_WORKSPACE_ID and OPENCODE_GO_AUTH_COOKIE to enable dashboard quota scraping." ); } finally { diff --git a/tests/unit/pack-artifact-entrypoint-closures.test.ts b/tests/unit/pack-artifact-entrypoint-closures.test.ts new file mode 100644 index 0000000000..94a037a68c --- /dev/null +++ b/tests/unit/pack-artifact-entrypoint-closures.test.ts @@ -0,0 +1,152 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { + APP_STAGING_ALLOWED_EXACT_PATHS, + PACK_ARTIFACT_REQUIRED_PATHS, +} from "../../scripts/build/pack-artifact-policy.ts"; + +// Generalization of pack-artifact-server-ws-closure.test.ts (#7065 class, 3rd recurrence: +// tls-options/3.8.41, head-response-guard VPS #7040 + npm #7065). assembleStandalone copies +// wrapper modules to the dist ROOT; the prepublish prune then deletes anything not in +// APP_STAGING_ALLOWED_EXACT_PATHS, and check:pack-artifact only fails for entries in +// PACK_ARTIFACT_REQUIRED_PATHS. The original test hardcoded ONE wrapper (server-ws.mjs) +// and ONE import form (static `from "./x"`). This suite derives the full closure from the +// sources of truth — EXTRA_MODULE_ENTRIES in assembleStandalone.mjs plus each wrapper's own +// imports (static, dynamic import() and require()) — so adding an import to ANY npm-shipped +// wrapper without updating both lists fails here instead of shipping a boot-crashing tarball. + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const ASSEMBLE = path.join(ROOT, "scripts", "build", "assembleStandalone.mjs"); +const BIN_ENTRY = path.join(ROOT, "bin", "omniroute.mjs"); + +interface WrapperEntry { + src: string; + dest: string; +} + +// EXTRA_MODULE_ENTRIES entries whose dest is a bare module filename at the dist root — +// these are the boot-path wrappers the prune can silently drop (the #7065 shape). +function distRootWrappers(): WrapperEntry[] { + const text = fs.readFileSync(ASSEMBLE, "utf8"); + const entries = [...text.matchAll(/src:\s*\[([^\]]+)\],?\s*dest:\s*\[([^\]]+)\]/gs)]; + const toPath = (segmentList: string) => + segmentList + .split(",") + .map((s) => s.trim().replace(/^"|"$/g, "")) + .filter(Boolean) + .join("/"); + return entries + .map((m) => ({ src: toPath(m[1]), dest: toPath(m[2]) })) + .filter((e) => !e.dest.includes("/") && /\.(mjs|cjs|js)$/.test(e.dest)); +} + +// Local sibling imports of a module: static `from "./x"`, dynamic `import("./x")`, +// and CommonJS `require("./x")`. The original test missed the dynamic form — server-ws +// boots dist/server.js via `await import("./server.js")`. +function localImports(filePath: string): string[] { + const src = fs.readFileSync(filePath, "utf8"); + const patterns = [ + /from\s+["']\.\/([^"']+)["']/g, + /import\(\s*["']\.\/([^"']+)["']\s*\)/g, + /require\(\s*["']\.\/([^"']+)["']\s*\)/g, + ]; + return [...new Set(patterns.flatMap((re) => [...src.matchAll(re)].map((m) => m[1])))]; +} + +/** Parent-relative specifiers (../) in a wrapper file — ALWAYS a packaging bug. */ +export function parentRelativeImports(src: string): string[] { + const patterns = [ + /from\s+["'](\.\.\/[^"']+)["']/g, + /import\(\s*["'](\.\.\/[^"']+)["']\s*\)/g, + /require\(\s*["'](\.\.\/[^"']+)["']\s*\)/g, + ]; + return [...new Set(patterns.flatMap((re) => [...src.matchAll(re)].map((m) => m[1])))]; +} + +// Wrappers that ship in the npm channel are exactly those whose dest survives the prune. +// Wrappers intentionally outside the npm tarball (e.g. healthcheck.mjs, Docker-only) are +// excluded: their imports live or die with them, consistently. +function npmShippedWrappers(): WrapperEntry[] { + return distRootWrappers().filter((e) => APP_STAGING_ALLOWED_EXACT_PATHS.includes(e.dest)); +} + +test("sanity: EXTRA_MODULE_ENTRIES parsing finds the known dist-root wrappers", () => { + const dests = distRootWrappers().map((e) => e.dest); + for (const known of ["server-ws.mjs", "peer-stamp.mjs", "head-response-guard.cjs"]) { + assert.ok(dests.includes(known), `parser lost known wrapper ${known}: got ${dests.join(", ")}`); + } + assert.ok(dests.length >= 7, `parsed only ${dests.length} dist-root wrappers`); +}); + +test("every local import of every npm-shipped wrapper survives the prune (allowlist)", () => { + for (const wrapper of npmShippedWrappers()) { + const srcPath = path.join(ROOT, wrapper.src); + assert.ok(fs.existsSync(srcPath), `EXTRA_MODULE_ENTRIES src missing on disk: ${wrapper.src}`); + const missing = localImports(srcPath).filter( + (f) => !APP_STAGING_ALLOWED_EXACT_PATHS.includes(f) + ); + assert.deepEqual( + missing, + [], + `${wrapper.dest}: add to APP_STAGING_ALLOWED_EXACT_PATHS: ${missing.join(", ")}` + ); + } +}); + +test("every local import of every npm-shipped wrapper is enforced by check:pack-artifact", () => { + for (const wrapper of npmShippedWrappers()) { + const missing = localImports(path.join(ROOT, wrapper.src)).filter( + (f) => !PACK_ARTIFACT_REQUIRED_PATHS.includes(`dist/${f}`) + ); + assert.deepEqual( + missing, + [], + `${wrapper.dest}: add dist/ to PACK_ARTIFACT_REQUIRED_PATHS: ${missing.join(", ")}` + ); + } +}); + +test("dynamic import() closure is covered (server-ws boots dist/server.js)", () => { + const serverWs = distRootWrappers().find((e) => e.dest === "server-ws.mjs"); + assert.ok(serverWs, "server-ws.mjs wrapper not found in EXTRA_MODULE_ENTRIES"); + const imports = localImports(path.join(ROOT, serverWs.src)); + assert.ok( + imports.includes("server.js"), + `dynamic import extraction broken — server.js not among: ${imports.join(", ")}` + ); +}); + +test("every bin/omniroute.mjs local import is enforced by check:pack-artifact", () => { + // The CLI boot path (bin/omniroute.mjs → bin/cli/*) is covered by allowlist PREFIXES, + // so a file vanishing from the tarball never fails the unexpected-paths check — only + // PACK_ARTIFACT_REQUIRED_PATHS makes its absence loud. Derive the requirement from + // the entrypoint's own imports. + const missing = localImports(BIN_ENTRY).filter( + (f) => !PACK_ARTIFACT_REQUIRED_PATHS.includes(`bin/${f}`) + ); + assert.deepEqual( + missing, + [], + `add bin/ to PACK_ARTIFACT_REQUIRED_PATHS: ${missing.join(", ")}` + ); +}); + +test("no npm-shipped wrapper uses a parent-relative (../) import — it escapes the package after the dist-root copy", () => { + // 2026-07-15 live incident: standalone-server-ws.mjs imported + // ../../src/shared/utils/runtimeTimeouts.ts (merged in #7191); copied to the dist + // root, the specifier resolved to node_modules/src/... OUTSIDE the package and + // every boot of the packed tarball crashed with ERR_MODULE_NOT_FOUND (#7065 + // class — caught by check:pack-boot). Wrapper dependencies must be SIBLINGS + // (./x.mjs) with their own EXTRA_MODULE_ENTRIES copy + pack allowlist entry. + for (const wrapper of npmShippedWrappers()) { + const escaping = parentRelativeImports(fs.readFileSync(path.join(ROOT, wrapper.src), "utf8")); + assert.deepEqual( + escaping, + [], + `${wrapper.src} has package-escaping imports: ${escaping.join(", ")} — extract to a sibling module instead` + ); + } +}); diff --git a/tests/unit/pack-artifact-policy.test.ts b/tests/unit/pack-artifact-policy.test.ts index a5f78e2235..9738342e7e 100644 --- a/tests/unit/pack-artifact-policy.test.ts +++ b/tests/unit/pack-artifact-policy.test.ts @@ -105,11 +105,14 @@ test("findMissingArtifactPaths flags missing root runtime files in the tarball", // alphabetically (bin/ < dist/ < scripts/ < src/), minus the paths present // above (dist/server.js, bin/omniroute.mjs, package.json, the postinstall scripts). assert.deepEqual(missingPaths, [ + "bin/cli/data-dir.mjs", "bin/cli/program.mjs", + "bin/cli/utils/storageKeyProvision.mjs", "bin/mcp-server.mjs", "bin/nodeRuntimeSupport.mjs", "dist/head-response-guard.cjs", "dist/http-method-guard.cjs", + "dist/main-server-timeouts.mjs", "dist/open-sse/services/compression/engines/rtk/filters/generic-output.json", "dist/open-sse/services/compression/rules/en/filler.json", "dist/peer-stamp.mjs", diff --git a/tests/unit/probe-6699-jules-executor-misroute.test.ts b/tests/unit/probe-6699-jules-executor-misroute.test.ts new file mode 100644 index 0000000000..a2072814f4 --- /dev/null +++ b/tests/unit/probe-6699-jules-executor-misroute.test.ts @@ -0,0 +1,40 @@ +// Probe for issue #6699 -- "Google Jules provider validation rejects a valid API key". +// +// A second reporter (MohammadMD1383) supplied screenshots showing that OmniRoute, when +// actually routing a chat-completion request for a saved "jules" connection, sends the +// request to https://api.openai.com/v1/chat/completions and surfaces OpenAI's own +// "Incorrect API key provided ... platform.openai.com" error -- even though the provider +// is displayed as JULES with target "jules/jules". This probe proves the executor-level +// root cause directly: getExecutor("jules") has no specialized executor and no REGISTRY +// entry, so DefaultExecutor's constructor silently falls back to PROVIDERS.openai, +// making buildUrl() return OpenAI's endpoint for a provider the user believes is Jules. +import test from "node:test"; +import assert from "node:assert/strict"; +import { getExecutor, hasSpecializedExecutor } from "../../open-sse/executors/index.ts"; + +test("#6699: jules has no specialized executor (falls through to DefaultExecutor)", () => { + assert.equal(hasSpecializedExecutor("jules"), false); +}); + +test("#6699: a chat-completion request routed to provider 'jules' must not silently hit OpenAI's endpoint", () => { + // Desired behavior: the Jules provider (a cloud-agent, registered only in + // CLOUD_AGENT_PROVIDERS/staticModels, never in the chat REGISTRY) must not silently + // resolve to OpenAI's chat/completions endpoint when routed through the normal + // chat-completions executor path. getExecutor() now throws a clear, sanitized error + // for this narrow set of chat-unsupported cloud-agent providers instead of falling + // through to DefaultExecutor's `PROVIDERS.openai` fallback (which produced the + // "Incorrect API key provided ... platform.openai.com" error the reporter saw for a + // genuine Jules key). Before the fix, getExecutor("jules") returned a working + // executor whose buildUrl() resolved to OpenAI's endpoint -- this assertion FAILS on + // unfixed release/v3.8.49 code because no error is thrown at all. + assert.throws( + () => getExecutor("jules"), + (err) => { + assert.match(err.message, /cloud-agent provider/i); + assert.match(err.message, /does not support direct chat completions/i); + assert.equal(err.status, 400); + return true; + }, + "provider 'jules' must raise a clear error instead of silently inheriting OpenAI's base URL/config" + ); +}); diff --git a/tests/unit/probe-6835-cyclebreaker.test.ts b/tests/unit/probe-6835-cyclebreaker.test.ts new file mode 100644 index 0000000000..fda653124d --- /dev/null +++ b/tests/unit/probe-6835-cyclebreaker.test.ts @@ -0,0 +1,28 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +test("getDbInstance() caps the probe-failed/restore cycle at 3 attempts (#6835)", async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-6835-")); + process.env.DATA_DIR = tmpDir; + const sqliteFile = path.join(tmpDir, "storage.sqlite"); + const backupFile = `${sqliteFile}.probe-failed-1000000000000`; + fs.writeFileSync(backupFile, Buffer.from("not a real sqlite file, always fails to open")); + const core = await import("../../src/lib/db/core.ts"); + const errors: string[] = []; + for (let i = 0; i < 6; i++) { + try { + core.getDbInstance(); + errors.push("(no error)"); + break; + } catch (err: unknown) { + errors.push(err instanceof Error ? err.message : String(err)); + } + } + const abortIndex = errors.findIndex((e) => e.includes("Aborting startup")); + assert.notEqual(abortIndex, -1, "Expected the cap to trip; got: " + errors.join(" | ")); + assert.ok(abortIndex <= 4, "Expected cap by call #4; took until #" + abortIndex); + fs.rmSync(tmpDir, { recursive: true, force: true }); +}); diff --git a/tests/unit/probe-6835-oom-uncapped.test.ts b/tests/unit/probe-6835-oom-uncapped.test.ts new file mode 100644 index 0000000000..9b91101646 --- /dev/null +++ b/tests/unit/probe-6835-oom-uncapped.test.ts @@ -0,0 +1,58 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +test("getDbInstance() eventually caps a persistently-OOMing sql.js probe (#6835)", async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-6835-oom-")); + process.env.DATA_DIR = tmpDir; + const sqliteFile = path.join(tmpDir, "storage.sqlite"); + fs.mkdirSync(sqliteFile); // forces better-sqlite3/node:sqlite to fail synchronously (EISDIR-style) + await import("../../src/lib/db/adapters/driverFactory.ts"); + const core = await import("../../src/lib/db/core.ts"); + const fakeAdapter = { + driver: "sql.js" as const, + open: true, + name: sqliteFile, + prepare() { + throw new Error("out of memory"); + }, + exec() { + throw new Error("out of memory"); + }, + pragma() { + throw new Error("out of memory"); + }, + transaction(fn: (...a: unknown[]) => T) { + return fn; + }, + immediate() {}, + async backup() {}, + checkpoint() {}, + close() {}, + raw: null, + }; + ( + globalThis as unknown as { __omnirouteSqlJsAdapters: Map } + ).__omnirouteSqlJsAdapters = new Map([[sqliteFile, fakeAdapter]]); + const errors: string[] = []; + for (let i = 0; i < 8; i++) { + try { + core.getDbInstance(); + errors.push("(no error)"); + break; + } catch (err: unknown) { + errors.push(err instanceof Error ? err.message : String(err)); + } + } + const anyAborted = errors.some((e) => e.includes("Aborting startup")); + assert.ok( + anyAborted, + "Expected getDbInstance() to eventually give up with a terminal " + + "'Aborting startup'-style diagnostic after repeated OOM probe failures, the same way it " + + "already does for generic corruption (#6632). Instead every call re-threw an identical, " + + "uncapped OOM error:\n" + errors.map((e, i) => ` [${i}] ${e}`).join("\n") + ); + fs.rmSync(tmpDir, { recursive: true, force: true }); +}); diff --git a/tests/unit/probe-7134-claude-web-empty-error-body.test.ts b/tests/unit/probe-7134-claude-web-empty-error-body.test.ts new file mode 100644 index 0000000000..b62659616a --- /dev/null +++ b/tests/unit/probe-7134-claude-web-empty-error-body.test.ts @@ -0,0 +1,102 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { writeFile } from "node:fs/promises"; + +// Issue #7134 — claude-web reported "Claude Web API error (400) with no +// response body" even when Claude's upstream DID send a real JSON error body. +// +// Root cause: tlsFetchStreaming() streams the upstream response to a temp +// file via tls-client-node's `streamOutputPath` mode. For a non-SSE, +// non-2xx response, the native binding resolves with an EMPTY in-memory +// `body` field (it only populates `body` for its non-streaming mode) even +// though the real error bytes were already written to the temp file and +// even peeked (`looksLikeSse`) to decide the response wasn't SSE. The old +// code read the empty `r.body` instead of the file it just peeked, throwing +// away the real upstream error detail. +// +// This test injects a fake `client` (matching the `{ request }` shape +// tlsFetchStreaming already accepts for DI) that reproduces the exact +// tls-client-node contract under `streamOutputPath`: write bytes to the file, +// resolve with an empty `body`. No `--experimental-test-module-mocks` flag +// needed — this exercises the real, unmodified `tlsFetchStreaming` via +// dependency injection instead of module-mocking `tls-client-node`. + +const { tlsFetchStreaming } = await import("../../open-sse/services/claudeTlsClient.ts"); + +const REAL_CLAUDE_ERROR_BODY = JSON.stringify({ + type: "error", + error: { + type: "invalid_request_error", + message: "This conversation UUID does not exist or you do not have access to it.", + }, +}); + +function makeFakeClient(status: number, bodyOnFile: string) { + return { + request: async (_url: string, opts: Record) => { + const streamOutputPath = opts.streamOutputPath as string; + await writeFile(streamOutputPath, bodyOnFile); + return { + status, + headers: {}, + // tls-client-node does not populate `body` for streamed requests — + // this is the exact defect condition. + body: "", + cookies: {}, + text: async () => "", + json: async () => ({}), + bytes: async () => new Uint8Array(), + }; + }, + }; +} + +test("issue #7134: tlsFetchStreaming surfaces the real error body for a non-SSE 400 under stream:true", async () => { + const client = makeFakeClient(400, REAL_CLAUDE_ERROR_BODY); + + const result = await tlsFetchStreaming( + client, + "https://claude.ai/api/organizations/x/chat_conversations/y/completion", + { method: "POST" }, + "[DONE]", + null, + 5_000 + ); + + assert.equal(result.status, 400); + assert.equal(result.body, null); + assert.ok( + result.text && result.text.includes("does not exist or you do not have access to it"), + `expected the real Claude error body to be surfaced, got: ${JSON.stringify(result.text)}` + ); +}); + +test("issue #7134: tlsFetchStreaming still uses r.body when the native client DOES populate it", async () => { + const client = { + request: async (_url: string, opts: Record) => { + const streamOutputPath = opts.streamOutputPath as string; + await writeFile(streamOutputPath, "{}"); + return { + status: 403, + headers: {}, + body: "populated body from native client", + cookies: {}, + text: async () => "", + json: async () => ({}), + bytes: async () => new Uint8Array(), + }; + }, + }; + + const result = await tlsFetchStreaming( + client, + "https://claude.ai/api/organizations/x/chat_conversations/y/completion", + { method: "POST" }, + "[DONE]", + null, + 5_000 + ); + + assert.equal(result.status, 403); + assert.equal(result.text, "populated body from native client"); +}); diff --git a/tests/unit/provider-models-vision-override-1904.test.ts b/tests/unit/provider-models-vision-override-1904.test.ts new file mode 100644 index 0000000000..508c6a1399 --- /dev/null +++ b/tests/unit/provider-models-vision-override-1904.test.ts @@ -0,0 +1,151 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +// #1904: manual vision-capability override for custom OpenAI-compatible models. +// +// detectVisionInput()/getCustomVisionCapabilityFields() already honour an explicit +// `supportsVision` flag when it is present on a custom-model record, but there was no +// way to *set* that flag from the dashboard's "Custom Models" add/edit form and no +// persistence path in the POST/PUT /api/provider-models handlers — so a user whose +// self-hosted backend doesn't self-report an image input modality (e.g. OpenRouter-style +// `architecture.input_modalities`) had no way to manually flag the model as +// vision-capable, exactly the report in the linked issue (Qwen-based custom vision +// model not showing the vision tag). +// +// This test proves the API round trip end-to-end: POST/PUT persist supportsVision on +// the custom-model row, GET surfaces it back, and getCustomVisionCapabilityFields() +// (what the /v1/models catalog calls) honours the explicit override. + +const TEST_DATA_DIR = fs.mkdtempSync( + path.join(os.tmpdir(), "omniroute-provider-model-vision-override-1904-") +); +process.env.DATA_DIR = TEST_DATA_DIR; + +const core = await import("../../src/lib/db/core.ts"); +const modelsDb = await import("../../src/lib/db/models.ts"); +const providerModelsRoute = await import("../../src/app/api/provider-models/route.ts"); +const catalogVision = await import("../../src/app/api/v1/models/catalogVision.ts"); + +async function resetStorage() { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); +} + +test.beforeEach(async () => { + await resetStorage(); +}); + +test.after(async () => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +function buildRequest(method: string, body: unknown) { + return new Request("http://localhost/api/provider-models", { + method, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); +} + +test("POST with supportsVision:true persists the flag on the custom model row", async () => { + const postRes = await providerModelsRoute.POST( + buildRequest("POST", { + provider: "openai-compatible-demo", + modelId: "qwen-vl-custom", + modelName: "Qwen VL Custom", + apiFormat: "chat-completions", + supportedEndpoints: ["chat"], + supportsVision: true, + }) + ); + const postBody = (await postRes.json()) as { model?: { supportsVision?: boolean } }; + assert.equal(postRes.status, 200); + assert.equal(postBody.model?.supportsVision, true); + + const models = await modelsDb.getCustomModels("openai-compatible-demo"); + const row = (models as Array<{ id?: string; supportsVision?: boolean }>).find( + (m) => m.id === "qwen-vl-custom" + ); + assert.ok(row, "model row should exist"); + assert.equal(row!.supportsVision, true); +}); + +test("PUT with supportsVision:true persists a manual override and PUT null clears it", async () => { + await modelsDb.addCustomModel( + "openai-compatible-demo", + "custom-local-model", + "Custom Local Model" + ); + + const putRes = await providerModelsRoute.PUT( + buildRequest("PUT", { + provider: "openai-compatible-demo", + modelId: "custom-local-model", + supportsVision: true, + }) + ); + const putBody = (await putRes.json()) as { model?: { supportsVision?: boolean } }; + assert.equal(putRes.status, 200); + assert.equal(putBody.model?.supportsVision, true); + + const getRes = await providerModelsRoute.GET( + new Request("http://localhost/api/provider-models?provider=openai-compatible-demo") + ); + const getBody = (await getRes.json()) as { + models: Array<{ id?: string; supportsVision?: boolean }>; + }; + const row = getBody.models.find((m) => m.id === "custom-local-model"); + assert.ok(row, "model row should be present"); + assert.equal(row!.supportsVision, true); + + // Clearing back to the id-based heuristic. + const clearRes = await providerModelsRoute.PUT( + buildRequest("PUT", { + provider: "openai-compatible-demo", + modelId: "custom-local-model", + supportsVision: null, + }) + ); + const clearBody = (await clearRes.json()) as { model?: { supportsVision?: boolean } }; + assert.equal(clearRes.status, 200); + assert.equal(clearBody.model?.supportsVision, undefined); +}); + +test("getCustomVisionCapabilityFields honours an explicit supportsVision:true override", () => { + const fields = catalogVision.getCustomVisionCapabilityFields( + { supportsVision: true }, + "openai-compatible-demo/qwen-not-heuristic-matched" + ); + assert.ok(fields, "explicit override should produce vision capability fields"); + assert.deepEqual(fields!.capabilities, { vision: true }); +}); + +test("getCustomVisionCapabilityFields honours an explicit supportsVision:false override even for a vision-like id", () => { + const fields = catalogVision.getCustomVisionCapabilityFields( + { supportsVision: false }, + "openai-compatible-demo/gpt-4-vision-preview" + ); + assert.equal(fields, null); +}); + +test("without an explicit flag, the UI has no field wired to persist supportsVision by default", async () => { + // Before this fix there was no request-shape carrying supportsVision at all; a plain + // add-model POST (matching the pre-fix form payload) must not silently mark a model + // vision-capable — the flag stays absent unless the user explicitly opts in. + const postRes = await providerModelsRoute.POST( + buildRequest("POST", { + provider: "openai-compatible-demo", + modelId: "plain-model", + apiFormat: "chat-completions", + supportedEndpoints: ["chat"], + }) + ); + const postBody = (await postRes.json()) as { model?: { supportsVision?: boolean } }; + assert.equal(postRes.status, 200); + assert.equal(postBody.model?.supportsVision, undefined); +}); diff --git a/tests/unit/provider-registry-qwen-vision.test.ts b/tests/unit/provider-registry-qwen-vision.test.ts index 9382027aed..1b2ddaa3f2 100644 --- a/tests/unit/provider-registry-qwen-vision.test.ts +++ b/tests/unit/provider-registry-qwen-vision.test.ts @@ -63,16 +63,17 @@ test("#2822 opencode-go/qwen3.6-plus deve ter supportsVision !== true", () => { ); }); -// #3328 — o oposto do #2822: MiniMax M3 (opencode) É multimodal (verificado -// empiricamente: descreve imagens base64 via o upstream opencode). Deve ter -// supportsVision: true para não ser barrado/strippado em requests com imagem. -test("#3328 opencode/minimax-m3-free deve ter supportsVision: true", () => { +// #3328 — o oposto do #2822: MiniMax M3 (opencode) era multimodal (verificado +// empiricamente: descrevia imagens base64 via o upstream opencode). #6998: +// minimax-m3-free foi deslistado do free tier da OpenCode Zen (401 "not +// supported") em 2026-07-14 e removido do catálogo estático — este teste +// agora confirma a remoção. +test("#6998 opencode/minimax-m3-free não deve mais estar registrado (deslistado upstream)", () => { const model = getModel("opencode", "minimax-m3-free"); - assert.ok(model, "minimax-m3-free deve estar registrado em opencode"); - assert.strictEqual( - model.supportsVision, - true, - "opencode/minimax-m3-free é multimodal — supportsVision deve ser true" + assert.equal( + model, + undefined, + "opencode/minimax-m3-free foi deslistado do free tier da OpenCode Zen (#6998)" ); }); diff --git a/tests/unit/provider-scoped-models-route.test.ts b/tests/unit/provider-scoped-models-route.test.ts index 55b40205cb..30662b0d19 100644 --- a/tests/unit/provider-scoped-models-route.test.ts +++ b/tests/unit/provider-scoped-models-route.test.ts @@ -10,16 +10,37 @@ process.env.DATA_DIR = TEST_DATA_DIR; const core = await import("../../src/lib/db/core.ts"); const providersDb = await import("../../src/lib/db/providers.ts"); const combosDb = await import("../../src/lib/db/combos.ts"); +const serviceModelsDb = await import("../../src/lib/db/serviceModels.ts"); const providerModelsRoute = await import("../../src/app/api/v1/providers/[provider]/models/route.ts"); +interface SeedConnectionOverrides { + authType?: string; + name?: string; + apiKey?: string | null; + accessToken?: string | null; + isActive?: boolean; + testStatus?: string; + providerSpecificData?: Record; +} + +type ProviderModelsResponse = { + data: Array>; + object?: string; + error?: { + message?: string; + code?: string; + type?: string; + }; +}; + async function resetStorage() { core.resetDbInstance(); fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); } -async function seedConnection(provider: string, overrides: Record = {}) { +async function seedConnection(provider: string, overrides: SeedConnectionOverrides = {}) { return providersDb.createProviderConnection({ provider, authType: overrides.authType || "apikey", @@ -62,8 +83,8 @@ test("provider models route returns only selected provider models with unprefixe } ); - const body = (await response.json()) as any; - const ids = body.data.map((model: any) => model.id); + const body = (await response.json()) as ProviderModelsResponse; + const ids = body.data.map((model) => String(model.id)); assert.equal(response.status, 200); assert.ok(ids.length > 0); @@ -72,7 +93,7 @@ test("provider models route returns only selected provider models with unprefixe false ); assert.equal( - body.data.some((model: any) => model.owned_by !== "openai"), + body.data.some((model) => String(model.owned_by) !== "openai"), false ); assert.equal(ids.includes("team-router"), false); @@ -93,8 +114,8 @@ test("provider models route accepts provider alias in path", async () => { } ); - const body = (await response.json()) as any; - const ids = body.data.map((model: any) => model.id); + const body = (await response.json()) as ProviderModelsResponse; + const ids = body.data.map((model) => String(model.id)); assert.equal(response.status, 200); assert.ok(ids.includes("claude-sonnet-4-6")); @@ -104,6 +125,42 @@ test("provider models route accepts provider alias in path", async () => { ); }); +test("provider models route supports service provider 9router", async () => { + serviceModelsDb.saveServiceModels("9router", [{ id: "gpt-4o-mini", name: "Local9R Test", available: true }]); + + const response = await providerModelsRoute.GET( + new Request("http://localhost/api/v1/providers/9router/models"), + { + params: Promise.resolve({ provider: "9router" }), + } + ); + + const body = (await response.json()) as ProviderModelsResponse; + const ids = body.data.map((model) => String(model.id)); + + assert.equal(response.status, 200); + assert.ok(ids.includes("gpt-4o-mini")); + assert.equal(ids.some((id: string) => id.includes("/")), false); +}); + +test("provider models route supports service provider cliproxyapi", async () => { + serviceModelsDb.saveServiceModels("cliproxyapi", [{ id: "llama-3", name: "Clip Test", available: true }]); + + const response = await providerModelsRoute.GET( + new Request("http://localhost/api/v1/providers/cliproxyapi/models"), + { + params: Promise.resolve({ provider: "cliproxyapi" }), + } + ); + + const body = (await response.json()) as ProviderModelsResponse; + const ids = body.data.map((model) => String(model.id)); + + assert.equal(response.status, 200); + assert.ok(ids.includes("llama-3")); + assert.equal(ids.some((id: string) => id.includes("/")), false); +}); + test("provider models route returns 400 for unknown provider", async () => { const response = await providerModelsRoute.GET( new Request("http://localhost/api/v1/providers/nope/models"), @@ -112,7 +169,7 @@ test("provider models route returns 400 for unknown provider", async () => { } ); - const body = (await response.json()) as any; + const body = (await response.json()) as ProviderModelsResponse; assert.equal(response.status, 400); assert.equal(body.error.code, "invalid_provider"); diff --git a/tests/unit/provider-validation-web-cookie-auth007.test.ts b/tests/unit/provider-validation-web-cookie-auth007.test.ts index c53705af04..344b1e865b 100644 --- a/tests/unit/provider-validation-web-cookie-auth007.test.ts +++ b/tests/unit/provider-validation-web-cookie-auth007.test.ts @@ -1,15 +1,18 @@ import test from "node:test"; import assert from "node:assert/strict"; -// The validator probes the provider's /models endpoint via safeOutboundFetch → -// fetchWithTimeout, which binds globalThis.fetch at MODULE LOAD time. The mock MUST be -// installed BEFORE importing validation.ts — a late reassignment (inside a test) is -// ignored and the validator hits the real network instead. (That made the 401/403 -// assertions pass only by coincidence — live chatgpt.com returns 401/403 — while the -// 200 case failed.) A mutable `nextResponse` lets each test vary the probe result, and -// `fetchCalls` proves the mocked probe ran rather than the live network. +// The validator probes the provider's /models endpoint via validationRead → safeOutboundFetch +// → fetchWithTimeout, which reads `globalThis.fetch` dynamically at CALL time (#7058 — routed +// through the proxy-aware patched fetch instead of a bypassing directHttpsRequest). Importing +// validation.ts pulls in the proxy-patch module, which installs its own `globalThis.fetch` +// exactly once at import time — so the mock must be (re)installed AFTER the import, not before, +// or the patch silently clobbers it. A mutable `nextResponse` lets each test vary the probe +// result, and `fetchCalls` proves the mocked probe ran rather than the live network. let nextResponse: { status: number; body: string } = { status: 200, body: "{}" }; let fetchCalls = 0; + +const { validateWebCookieProvider } = await import("../../src/lib/providers/validation.ts"); + globalThis.fetch = (async () => { fetchCalls++; return new Response(nextResponse.body, { @@ -18,8 +21,6 @@ globalThis.fetch = (async () => { }); }) as typeof fetch; -const { validateWebCookieProvider } = await import("../../src/lib/providers/validation.ts"); - function mockFetch(status: number, body: string) { nextResponse = { status, body }; fetchCalls = 0; diff --git a/tests/unit/providers-yuanbao-web.test.ts b/tests/unit/providers-yuanbao-web.test.ts index 2feab63371..48477c83d4 100644 --- a/tests/unit/providers-yuanbao-web.test.ts +++ b/tests/unit/providers-yuanbao-web.test.ts @@ -71,6 +71,13 @@ async function readStreamText(res: Response): Promise { } test("missing hy_token cookie returns a 401 auth error", async () => { + // Hermetic: the 401 must come from the executor's own cookie validation, never + // from the real upstream — on GitHub-hosted runners the Tencent endpoint is + // unreachable and a live call turns this into a 71s 502 false-negative. + const original = globalThis.fetch; + globalThis.fetch = (async () => { + throw new Error("network disabled in this test — executor must reject before fetching"); + }) as typeof fetch; const exec = new YuanbaoWebExecutor(); const { response } = await exec.execute({ model: "deepseek-v3", @@ -84,6 +91,7 @@ test("missing hy_token cookie returns a 401 auth error", async () => { assert.match(body.error.message, /hy_user|hy_token|session cookie/); // Never leak stack traces. assert.ok(!body.error.message.includes("at /")); + globalThis.fetch = original; }); test("streaming request translates think/text events into OpenAI chunks", async () => { diff --git a/tests/unit/quota-card-grid-density-6815.test.ts b/tests/unit/quota-card-grid-density-6815.test.ts new file mode 100644 index 0000000000..bafc3d6b3b --- /dev/null +++ b/tests/unit/quota-card-grid-density-6815.test.ts @@ -0,0 +1,190 @@ +// #6815 — Provider Quota page horizontal density. +// +// #6815 changed QuotaCardGrid.tsx's per-group card grid from a +// single-column-only layout (`flex flex-col`) to one that packs multiple +// QuotaCards side by side on wide screens, instead of stacking every card +// vertically no matter how much horizontal space is available. +// +// That guarantee was only ever asserted *incidentally*, by two other guards +// that pinned the literal Tailwind token the #6815 implementation happened +// to use at the time (`sm:grid-cols-2` in +// tests/unit/quota-card-grid-mobile-7072.test.ts and +// tests/unit/quota-card-grid-horizontal-layout.test.ts). When PR #7027 +// migrated the component from a fixed breakpoint ladder +// (`grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4`) to a +// container-driven auto-fit template +// (`grid-cols-[repeat(auto-fit,minmax(min(100%,280px),1fr))]`), that literal +// token disappeared from the source and both guards were edited to stop +// asserting it — silently deleting the only coverage #6815 had. +// +// This guard re-establishes dedicated coverage for the #6815 density +// guarantee itself, decoupled from *how* the component achieves it. Instead +// of matching a specific class-name token, it simulates, from the shipped +// className(s), how many columns the per-group card grid would actually +// render at a wide container width — supporting both mechanisms seen in this +// component's history (a Tailwind breakpoint ladder, and a CSS auto-fit +// `minmax()` template) — and asserts that count is >1. Reverting to a single +// unconditional column (`grid-cols-1` with no responsive/auto-fit variants, +// or dropping the grid entirely for `flex flex-col`) must fail this guard, +// regardless of which mechanism produced the regression. + +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import ts from "typescript"; + +const COMPONENT_PATH = path.resolve( + import.meta.dirname, + "../../src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaCardGrid.tsx" +); + +/** + * Extract the string literal passed to `className={...}` (or `className="..."`) + * for every JSX `
` opening element in the component's source, in source + * order, via the TypeScript compiler API (not a hand-rolled regex — tracks + * the real AST so it can't be fooled by comments/whitespace). + */ +function extractDivClassNames(sourcePath: string): string[] { + const sourceText = fs.readFileSync(sourcePath, "utf8"); + const sourceFile = ts.createSourceFile( + sourcePath, + sourceText, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TSX + ); + const classNames: string[] = []; + + function visit(node: ts.Node) { + if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) { + const tagName = node.tagName.getText(sourceFile); + if (tagName === "div") { + for (const attr of node.attributes.properties) { + if (ts.isJsxAttribute(attr) && attr.name.getText(sourceFile) === "className") { + const init = attr.initializer; + if (init && ts.isStringLiteral(init)) { + classNames.push(init.text); + } else if ( + init && + ts.isJsxExpression(init) && + init.expression && + ts.isStringLiteral(init.expression) + ) { + classNames.push(init.expression.text); + } + } + } + } + } + ts.forEachChild(node, visit); + } + visit(sourceFile); + return classNames; +} + +// Tailwind's default min-width breakpoints (px). Unprefixed utilities apply +// at every width (breakpoint 0) and later/larger breakpoints win the cascade +// once their min-width is met, mirroring Tailwind's mobile-first source order. +const TAILWIND_BREAKPOINTS: Record = { + sm: 640, + md: 768, + lg: 1024, + xl: 1280, + "2xl": 1536, +}; + +type ColumnRule = + | { breakpoint: number; kind: "fixed"; columns: number } + | { breakpoint: number; kind: "autofit"; trackPx: number }; + +/** + * Parse every `grid-cols-*` utility (optionally breakpoint-prefixed) found in + * a className string into a column rule, supporting both mechanisms this + * component has shipped with: + * - a fixed count, e.g. `grid-cols-2`, `md:grid-cols-3` + * - a CSS auto-fit template, e.g. + * `grid-cols-[repeat(auto-fit,minmax(min(100%,280px),1fr))]`, from which + * the minimum track width (in px) is extracted. + */ +function parseColumnRules(className: string): ColumnRule[] { + const rules: ColumnRule[] = []; + for (const token of className.split(/\s+/).filter(Boolean)) { + const prefixMatch = token.match(/^(?:([a-zA-Z0-9]+):)?grid-cols-(.+)$/); + if (!prefixMatch) continue; + const [, prefix, rest] = prefixMatch; + const breakpoint = prefix ? (TAILWIND_BREAKPOINTS[prefix] ?? 0) : 0; + + if (/^\d+$/.test(rest)) { + rules.push({ breakpoint, kind: "fixed", columns: parseInt(rest, 10) }); + continue; + } + + const autoFitMatch = rest.match(/^\[repeat\(auto-fit,\s*minmax\((.+),\s*1fr\)\)\]$/); + if (autoFitMatch) { + const trackPxMatches = [...autoFitMatch[1].matchAll(/(\d+)px/g)]; + if (trackPxMatches.length > 0) { + const trackPx = parseInt(trackPxMatches[trackPxMatches.length - 1][1], 10); + rules.push({ breakpoint, kind: "autofit", trackPx }); + } + } + } + return rules; +} + +/** + * Given a className string, estimate how many columns the grid renders at a + * given container/viewport width, by picking the widest matching breakpoint + * rule (Tailwind cascade) and resolving fixed vs. auto-fit tracks. Returns 1 + * (single column) when no `grid-cols-*` rule is present at all — e.g. a + * `flex flex-col` layout. + */ +function estimateColumnsAtWidth(className: string, widthPx: number): number { + const rules = parseColumnRules(className).filter((r) => r.breakpoint <= widthPx); + if (rules.length === 0) return 1; + const active = rules.reduce((best, r) => (r.breakpoint >= best.breakpoint ? r : best)); + if (active.kind === "fixed") return active.columns; + return Math.max(1, Math.floor(widthPx / active.trackPx)); +} + +// --- Self-test of the estimator against known-good and known-bad shapes --- +// (independent of the real component, so the simulation logic itself is +// pinned before it's trusted to judge the shipped source below). + +test("#6815 density estimator — breakpoint ladder resolves to multiple columns on a wide viewport", () => { + const className = "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-3"; + assert.equal(estimateColumnsAtWidth(className, 1200), 3); + assert.equal(estimateColumnsAtWidth(className, 375), 1); +}); + +test("#6815 density estimator — auto-fit template resolves to multiple columns on a wide container", () => { + const className = "grid grid-cols-[repeat(auto-fit,minmax(min(100%,280px),1fr))] gap-3"; + assert.ok(estimateColumnsAtWidth(className, 1200) >= 2); + assert.equal(estimateColumnsAtWidth(className, 200), 1); +}); + +test("#6815 density estimator — single unconditional column stays at 1 column regardless of width", () => { + assert.equal(estimateColumnsAtWidth("grid grid-cols-1 gap-3", 1920), 1); +}); + +test("#6815 density estimator — flex column stack (no grid-cols) resolves to 1 column", () => { + assert.equal(estimateColumnsAtWidth("flex flex-col gap-3", 1920), 1); +}); + +// --- The actual regression guard, reading the shipped component source --- + +test("QuotaCardGrid (#6815) — per-group card grid renders multiple columns on a wide container", () => { + const classNames = extractDivClassNames(COMPONENT_PATH); + const cardGridClassName = classNames.find((c) => /\bgrid\b/.test(c) && /grid-cols-/.test(c)); + assert.ok( + cardGridClassName, + "expected to find a grid-based per-group card grid className (not a single-column flex stack)" + ); + + const columnsOnWideContainer = estimateColumnsAtWidth(cardGridClassName!, 1200); + assert.ok( + columnsOnWideContainer > 1, + `expected the per-group card grid to render more than 1 column at 1200px, got ${columnsOnWideContainer} ` + + `from className="${cardGridClassName}" — this is the #6815 density regression` + ); +}); diff --git a/tests/unit/quota-card-grid-horizontal-layout.test.ts b/tests/unit/quota-card-grid-horizontal-layout.test.ts index 9966034ba9..5e9fcd52ba 100644 --- a/tests/unit/quota-card-grid-horizontal-layout.test.ts +++ b/tests/unit/quota-card-grid-horizontal-layout.test.ts @@ -6,8 +6,11 @@ // the shipped JSX structure and grouping logic directly: // 1. Grouping still produces one header per distinct provider with the // correct account count ("N account(s)"). -// 2. The per-group card grid starts multi-column (`grid-cols-2`), not -// single-column, so cards fill horizontal space sooner. +// 2. The per-group card grid keeps a single-column mobile fallback +// (`grid-cols-1`) below the `sm:` breakpoint (restored by #7072 after +// PR #6815 dropped it and clipped labels on phone-width viewports), +// while still going multi-column (`sm:grid-cols-2`) from `sm:` up so +// cards fill horizontal space sooner on larger screens. // 3. Provider groups themselves flow into multiple columns on wide screens // (`columns-*`) instead of an unconditional vertical `flex flex-col` // stack. @@ -104,14 +107,14 @@ test("QuotaCardGrid (#3520) — outer container flows groups into multiple colum assert.notEqual(outerClassName, "flex flex-col gap-6"); }); -test("QuotaCardGrid (#3520) — per-group card grid starts multi-column (grid-cols-2), not single-column", () => { +test("QuotaCardGrid (#3520/#7072) — per-group card grid keeps a mobile grid-cols-1 fallback and goes multi-column from sm: up", () => { const classNames = extractDivClassNames(COMPONENT_PATH); const cardGridClassName = classNames.find( (c) => /\bgrid\b/.test(c) && /grid-cols-/.test(c) ); assert.ok(cardGridClassName, "expected to find the per-group card grid's className"); - assert.match(cardGridClassName!, /\bgrid-cols-2\b/); - assert.doesNotMatch(cardGridClassName!, /\bgrid-cols-1\b/); + assert.match(cardGridClassName!, /\bgrid-cols-1\b/); + assert.match(cardGridClassName!, /\bsm:grid-cols-2\b/); }); test("QuotaCardGrid (#3520) — early-returns null when there are no connections", () => { diff --git a/tests/unit/quota-card-grid-mobile-7072.test.ts b/tests/unit/quota-card-grid-mobile-7072.test.ts new file mode 100644 index 0000000000..aeede949a8 --- /dev/null +++ b/tests/unit/quota-card-grid-mobile-7072.test.ts @@ -0,0 +1,79 @@ +// #7072 — Provider Quota page card grid clipped on mobile. +// +// PR #6815 changed QuotaCardGrid.tsx's per-group card grid from +// `grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4` to +// `grid-cols-2 md:grid-cols-3 xl:grid-cols-4`, dropping the mobile (<768px) +// single-column fallback that every other card-grid in the dashboard still +// has (ProviderQuotaWidget.tsx, EvalsTab.tsx, MediaPageClient.tsx, +// SystemStorageTab.tsx). Forcing 2 columns even on phone widths squeezes +// each QuotaCard, and since QuotaCard's outer Card uses `overflow-hidden`, +// the overflowing button/label text is clipped instead of wrapping. +// +// This regression guard parses QuotaCardGrid.tsx's JSX via the TypeScript +// compiler API and asserts the per-group card grid's className restores an +// unprefixed `grid-cols-1` mobile fallback, while keeping the #6815 density +// gains (grid-cols-2 at `sm:` and up). + +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import ts from "typescript"; + +const COMPONENT_PATH = path.resolve( + import.meta.dirname, + "../../src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaCardGrid.tsx" +); + +function extractDivClassNames(sourcePath: string): string[] { + const sourceText = fs.readFileSync(sourcePath, "utf8"); + const sourceFile = ts.createSourceFile( + sourcePath, + sourceText, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TSX + ); + const classNames: string[] = []; + + function visit(node: ts.Node) { + if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) { + const tagName = node.tagName.getText(sourceFile); + if (tagName === "div") { + for (const attr of node.attributes.properties) { + if (ts.isJsxAttribute(attr) && attr.name.getText(sourceFile) === "className") { + const init = attr.initializer; + if (init && ts.isStringLiteral(init)) { + classNames.push(init.text); + } else if ( + init && + ts.isJsxExpression(init) && + init.expression && + ts.isStringLiteral(init.expression) + ) { + classNames.push(init.expression.text); + } + } + } + } + } + ts.forEachChild(node, visit); + } + visit(sourceFile); + return classNames; +} + +test("QuotaCardGrid (#7072) — per-group card grid keeps a single-column mobile fallback", () => { + const classNames = extractDivClassNames(COMPONENT_PATH); + const cardGridClassName = classNames.find((c) => /\bgrid\b/.test(c) && /grid-cols-/.test(c)); + assert.ok(cardGridClassName, "expected to find the per-group card grid's className"); + + const tokens = cardGridClassName!.split(/\s+/); + const unprefixedGridCols = tokens.find((t) => /^grid-cols-\d+$/.test(t)); + assert.equal( + unprefixedGridCols, + "grid-cols-1", + `expected unprefixed grid-cols-1 (mobile fallback), got className="${cardGridClassName}"` + ); + assert.match(cardGridClassName!, /\bsm:grid-cols-2\b/, "expected sm:grid-cols-2 to be preserved"); +}); diff --git a/tests/unit/reasoningContentInjector.test.ts b/tests/unit/reasoningContentInjector.test.ts new file mode 100644 index 0000000000..134843e774 --- /dev/null +++ b/tests/unit/reasoningContentInjector.test.ts @@ -0,0 +1,44 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; + +import { + isThinkingMessageModel, + injectReasoningContentForThinkingModel, +} from "../../open-sse/utils/reasoningContentInjector.ts"; + +describe("reasoningContentInjector — xiaomi-tokenplan mimo family (9router#1321)", () => { + it("recognizes xiaomi-tokenplan/mimo-v2.5-pro as a thinking-mode model", () => { + assert.equal(isThinkingMessageModel("xiaomi-tokenplan/mimo-v2.5-pro"), true); + }); + + it("recognizes bare mimo model ids as thinking-mode models", () => { + assert.equal(isThinkingMessageModel("mimo-v2.5-pro"), true); + }); + + it("still recognizes the existing thinking-mode families (deepseek/kimi/k2/minimax)", () => { + assert.equal(isThinkingMessageModel("deepseek-v4-flash"), true); + assert.equal(isThinkingMessageModel("kimi-k2"), true); + assert.equal(isThinkingMessageModel("minimax-m2"), true); + }); + + it("does not flag unrelated model ids", () => { + assert.equal(isThinkingMessageModel("gpt-4o"), false); + }); + + it("injects a reasoning_content placeholder for assistant messages when routed to mimo", () => { + const body = { + model: "xiaomi-tokenplan/mimo-v2.5-pro", + messages: [ + { role: "user", content: "hi" }, + { role: "assistant", content: "hello" }, + ], + }; + + // Simulate the executor gate: only inject when the model is a thinking model. + assert.equal(isThinkingMessageModel(body.model), true); + + const result = injectReasoningContentForThinkingModel(body) as typeof body; + const assistantMsg = result.messages[1] as Record; + assert.equal(assistantMsg.reasoning_content, " "); + }); +}); diff --git a/tests/unit/refactor-opencodeHeaders.test.ts b/tests/unit/refactor-opencodeHeaders.test.ts index b79865b30e..19a5a1cdf6 100644 --- a/tests/unit/refactor-opencodeHeaders.test.ts +++ b/tests/unit/refactor-opencodeHeaders.test.ts @@ -116,6 +116,43 @@ describe("forwardOpencodeClientHeaders – x-opencode-* headers", () => { }); }); +// ── agent metadata headers (X-Session-ID / X-Title) — 9router#2413 ───────── +// Non-OpenCode agent clients (e.g. custom providers) commonly send X-Session-ID +// and X-Title for upstream request tracking/attribution. These were previously +// dropped for every client outside the x-opencode-* allowlist. + +describe("forwardOpencodeClientHeaders – X-Session-ID / X-Title", () => { + it("forwards X-Session-ID from client headers", () => { + const headers = h(); + const clientHeaders = { "X-Session-ID": "sess-xyz" }; + forwardOpencodeClientHeaders(headers, clientHeaders); + assert.equal(headers["x-session-id"], "sess-xyz"); + }); + + it("forwards X-Title from client headers", () => { + const headers = h(); + const clientHeaders = { "X-Title": "My Agent" }; + forwardOpencodeClientHeaders(headers, clientHeaders); + assert.equal(headers["x-title"], "My Agent"); + }); + + it("matches X-Session-ID / X-Title case-insensitively", () => { + const headers = h(); + const clientHeaders = { "x-session-id": "sess-lower", "x-title": "lower title" }; + forwardOpencodeClientHeaders(headers, clientHeaders); + assert.equal(headers["x-session-id"], "sess-lower"); + assert.equal(headers["x-title"], "lower title"); + }); + + it("still does NOT forward unrelated unknown headers", () => { + const headers = h(); + const clientHeaders = { "X-Session-ID": "sess-1", "X-Random-Other": "nope" }; + forwardOpencodeClientHeaders(headers, clientHeaders); + assert.equal(headers["x-session-id"], "sess-1"); + assert.equal(headers["X-Random-Other"], undefined); + }); +}); + // ── synthesizeRequestId ───────────────────────────────────────────────────── describe("forwardOpencodeClientHeaders – synthesizeRequestId", () => { diff --git a/tests/unit/rehome-open-prs.test.ts b/tests/unit/rehome-open-prs.test.ts new file mode 100644 index 0000000000..5b5c7d23df --- /dev/null +++ b/tests/unit/rehome-open-prs.test.ts @@ -0,0 +1,56 @@ +// Guard for scripts/release/rehome-open-prs.mjs — the parallel-cycle PR re-home +// (generate-release Phase 0a.0b step 3). +// +// The script exists because `gh pr edit --base` fails SILENTLY (v3.8.42): it exits 0 +// while leaving the base untouched. The network side of that is verified by a read-back +// in the script itself; what is testable here is the decision — which PRs get retargeted +// and which are left alone. Getting that wrong in either direction is expensive: +// - retargeting a PR that was never on the frozen branch drags unrelated work into the cycle +// - skipping one strands a contributor on a branch the captain has taken over + +import test from "node:test"; +import assert from "node:assert/strict"; + +import { classify } from "../../scripts/release/rehome-open-prs.mjs"; + +const FROZEN = "release/v3.8.49"; +const NEXT = "release/v3.8.50"; + +test("retargets an open PR sitting on the frozen release branch", () => { + const { action } = classify( + { number: 1, baseRefName: FROZEN, isDraft: false }, + FROZEN, + NEXT + ); + assert.equal(action, "retarget"); +}); + +test("retargets a DRAFT on the frozen branch — a draft still needs a live base", () => { + const { action } = classify({ number: 2, baseRefName: FROZEN, isDraft: true }, FROZEN, NEXT); + assert.equal(action, "retarget"); +}); + +test("skips a PR already re-homed — the script must be idempotent for a resumed release", () => { + const { action, reason } = classify( + { number: 3, baseRefName: NEXT, isDraft: false }, + FROZEN, + NEXT + ); + assert.equal(action, "skip"); + assert.match(reason, /already re-homed/); +}); + +test("never touches a PR based on main — that is the release PR's own lane", () => { + const { action, reason } = classify({ number: 4, baseRefName: "main", isDraft: false }, FROZEN, NEXT); + assert.equal(action, "skip"); + assert.match(reason, /not the frozen branch/); +}); + +test("never touches a PR based on an older, already-shipped release", () => { + const { action } = classify( + { number: 5, baseRefName: "release/v3.8.47", isDraft: false }, + FROZEN, + NEXT + ); + assert.equal(action, "skip"); +}); diff --git a/tests/unit/service-backends.test.ts b/tests/unit/service-backends.test.ts new file mode 100644 index 0000000000..e04aa40010 --- /dev/null +++ b/tests/unit/service-backends.test.ts @@ -0,0 +1,20 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { + isServiceBackendPluginId, + getServiceToolFromPluginId, + SERVICE_BACKEND_PLUGIN_IDS, +} from "../../src/lib/services/serviceBackends"; + +test("service backend helper recognizes embedded service plugin ids", () => { + for (const pluginId of SERVICE_BACKEND_PLUGIN_IDS) { + assert.equal(isServiceBackendPluginId(pluginId), true); + } +}); + +test("service backend helper maps plugin ids to runtime tool ids", () => { + assert.equal(getServiceToolFromPluginId("9router"), "9router"); + assert.equal(getServiceToolFromPluginId("cliproxyapi"), "cliproxy"); + assert.equal(getServiceToolFromPluginId("native"), undefined); +}); diff --git a/tests/unit/services/end-to-end-shape.test.ts b/tests/unit/services/end-to-end-shape.test.ts index 80b96725e7..2e27739869 100644 --- a/tests/unit/services/end-to-end-shape.test.ts +++ b/tests/unit/services/end-to-end-shape.test.ts @@ -328,6 +328,7 @@ describe("cliproxy — response shapes", () => { assert.ok("installedVersion" in b, "cliproxy status must include installedVersion"); assert.ok("updateAvailable" in b, "cliproxy status must include updateAvailable"); assert.ok("autoStart" in b, "cliproxy status must include autoStart"); + assert.ok("providerExpose" in b, "cliproxy status must include providerExpose"); assert.ok(typeof b.updateAvailable === "boolean", "updateAvailable must be boolean"); }); }); diff --git a/tests/unit/services/installers/ninerouter.test.ts b/tests/unit/services/installers/ninerouter.test.ts index fd56a9dd0d..4554e34d3e 100644 --- a/tests/unit/services/installers/ninerouter.test.ts +++ b/tests/unit/services/installers/ninerouter.test.ts @@ -7,6 +7,7 @@ import { execSync } from "node:child_process"; const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-installer-")); const FAKE_BIN_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-fake-bin-")); +const MOCK_NINEROUTER_VERSION = "0.5.30"; process.env.DATA_DIR = TEST_DATA_DIR; process.env.NODE_ENV = "test"; @@ -34,12 +35,12 @@ if [ "$CMD" = "install" ]; then if [ -z "$PREFIX" ]; then PREFIX="$npm_config_prefix"; fi PKG_DIR="$PREFIX/node_modules/9router" mkdir -p "$PKG_DIR/app" - echo '{"name":"9router","version":"0.4.59"}' > "$PKG_DIR/package.json" + echo '{"name":"9router","version":"${MOCK_NINEROUTER_VERSION}"}' > "$PKG_DIR/package.json" touch "$PKG_DIR/app/server.js" exit 0 fi if [ "$CMD" = "view" ]; then - echo "0.4.59" + echo "${MOCK_NINEROUTER_VERSION}" exit 0 fi exit 0 @@ -76,7 +77,7 @@ test.after(() => { }); test("install creates package.json structure", async () => { - const result = await install("0.4.59"); + const result = await install(MOCK_NINEROUTER_VERSION); // Host package.json must exist const hostPkg = path.join(NINEROUTER_INSTALL_DIR, "package.json"); @@ -88,19 +89,19 @@ test("install creates package.json structure", async () => { assert.equal(parsedHost.name, "omniroute-9router-host"); assert.ok(parsedHost.private); - assert.equal(result.installedVersion, "0.4.59"); + assert.equal(result.installedVersion, MOCK_NINEROUTER_VERSION); assert.equal(result.installPath, NINEROUTER_INSTALL_DIR); assert.ok(result.durationMs >= 0); }); test("install captures real version from node_modules/9router/package.json", async () => { const ver = await getInstalledVersion(); - assert.equal(ver, "0.4.59", "should read version from installed package"); + assert.equal(ver, MOCK_NINEROUTER_VERSION, "should read version from installed package"); }); test("update calls npm install with latest (idempotent)", async () => { const result = await update(); - assert.equal(result.installedVersion, "0.4.59"); + assert.equal(result.installedVersion, MOCK_NINEROUTER_VERSION); }); test("uninstall removes node_modules and marks not_installed in DB", async () => { @@ -127,7 +128,7 @@ test("uninstall removes node_modules and marks not_installed in DB", async () => test("getLatestVersion returns version string from npm view", async () => { const ver = await getLatestVersion(); - assert.equal(ver, "0.4.59"); + assert.equal(ver, MOCK_NINEROUTER_VERSION); }); test("resolveSpawnArgs returns expected env and command", () => { diff --git a/tests/unit/shared/components/ProxyConfigModal.test.tsx b/tests/unit/shared/components/ProxyConfigModal.test.tsx index 12299359d1..a63ebe9801 100644 --- a/tests/unit/shared/components/ProxyConfigModal.test.tsx +++ b/tests/unit/shared/components/ProxyConfigModal.test.tsx @@ -389,3 +389,66 @@ describe("ProxyConfigModal custom registry saves", () => { ).toBe(false); }); }); + +describe("ProxyConfigModal test connection (saved proxy)", () => { + beforeEach(() => { + ( + globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean } + ).IS_REACT_ACT_ENVIRONMENT = true; + fetchCalls = []; + }); + + afterEach(() => { + while (cleanupCallbacks.length > 0) { + cleanupCallbacks.pop()?.(); + } + document.body.innerHTML = ""; + vi.unstubAllGlobals(); + vi.restoreAllMocks(); + }); + + it("includes proxyId when testing a saved SOCKS5 registry proxy so the server can load its stored credentials", async () => { + installFetchMock((url, init) => { + const method = String(init?.method || "GET").toUpperCase(); + if (method === "GET" && url === "/api/settings/proxies") { + return { + body: { + items: [ + { + id: "socks5-1", + name: "Geonode SOCKS5", + type: "socks5", + host: "proxy.geonode.io", + port: 12000, + username: "***", + password: "***", + source: "manual", + }, + ], + total: 1, + socks5Enabled: true, + }, + }; + } + if (url.startsWith("/api/settings/proxies/assignments?") && url.includes("scope=provider")) { + return { + body: { items: [{ proxyId: "socks5-1", scope: "provider", scopeId: "claude" }], total: 1 }, + }; + } + if (method === "POST" && url === "/api/settings/proxy/test") { + return { body: { success: true, publicIp: "1.2.3.4", latencyMs: 500 } }; + } + return defaultProxyConfigResponses(url) || { status: 404, body: {} }; + }); + + const { container } = await renderProxyConfigModal(); + await clickButton(container, "testConnection"); + await waitForCall((call) => call.method === "POST" && call.url === "/api/settings/proxy/test"); + + const testCall = fetchCalls.find( + (call) => call.method === "POST" && call.url === "/api/settings/proxy/test" + ); + expect(testCall).toBeTruthy(); + expect(testCall?.body?.proxyId).toBe("socks5-1"); + }, 20000); +}); diff --git a/tests/unit/standalone-server-ws-keepalive-timeout-7003.test.ts b/tests/unit/standalone-server-ws-keepalive-timeout-7003.test.ts new file mode 100644 index 0000000000..b69784738e --- /dev/null +++ b/tests/unit/standalone-server-ws-keepalive-timeout-7003.test.ts @@ -0,0 +1,77 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +// #7003 — the RED/GREEN spec in main-server-keepalive-timeout-7003.test.ts proves +// getMainServerTimeoutConfig() raises keepAliveTimeout/headersTimeout above Node's +// unconfigured 5_000ms default, and that the original fix wired it into +// scripts/dev/run-next.mjs. But run-next.mjs only runs `npm run dev`/`npm start` +// from a source checkout. The server real end users run — `omniroute serve` +// (npm-installed CLI), Docker, and Electron — spawns the standalone Next build's +// server.js via scripts/dev/run-standalone.mjs, which prefers server-ws.mjs +// (built from scripts/dev/standalone-server-ws.mjs, copied byte-for-byte into +// dist/server-ws.mjs by scripts/build/assembleStandalone.mjs) over the bare +// server.js specifically because it wraps `http.createServer` with production +// behavior the bare server lacks (peer-IP stamping, method/HEAD guards, WS +// proxying, TLS). Before this fix, that wrapper left Node's http.Server +// keepAliveTimeout/headersTimeout at their unconfigured defaults, so the +// JetBrains AI Assistant reconnect bug reproduced by main-server-keepalive-timeout +// -7003.test.ts still hit the production entry point every real user runs. +// +// standalone-server-ws.mjs has top-level side effects (monkeypatches +// http.createServer, generates a random UUID, and unconditionally +// `await import("./server.js")` — a file that only exists in the assembled +// standalone output, not in the source tree) so it cannot be imported +// in-process. Guard the fix by inspecting the source, mirroring the pattern +// used for run-next.mjs in run-next-node-env.test.ts. +const here = path.dirname(fileURLToPath(import.meta.url)); +const source = fs.readFileSync( + path.resolve(here, "../../scripts/dev/standalone-server-ws.mjs"), + "utf8" +); + +test("standalone-server-ws.mjs imports getMainServerTimeoutConfig", () => { + // The wrapper must import the SIBLING ./main-server-timeouts.mjs — a + // ../../src/... import escapes the package after the dist-root copy and + // crashed every packed boot (2026-07-15, #7065 class). Parity with the + // canonical runtimeTimeouts.ts is guarded by main-server-timeouts-parity.test.ts. + assert.match( + source, + /import\s*\{\s*getMainServerTimeoutConfig\s*\}\s*from\s*["']\.\/main-server-timeouts\.mjs["']/, + "expected the production server wrapper to import getMainServerTimeoutConfig " + + "from its shipped sibling module (./main-server-timeouts.mjs)" + ); +}); + +test("standalone-server-ws.mjs applies keepAliveTimeout/headersTimeout to the wrapped server", () => { + assert.match( + source, + /server\.keepAliveTimeout\s*=\s*\w*[Tt]imeouts?\.keepAliveTimeoutMs/, + "expected the wrapped server object to have keepAliveTimeout set from getMainServerTimeoutConfig()" + ); + assert.match( + source, + /server\.headersTimeout\s*=\s*\w*[Tt]imeouts?\.headersTimeoutMs/, + "expected the wrapped server object to have headersTimeout set from getMainServerTimeoutConfig()" + ); +}); + +test("keepAliveTimeout/headersTimeout are applied inside createServerWithResponsesWs, before the server is returned", () => { + const factoryIdx = source.search(/function createServerWithResponsesWs/); + const keepAliveIdx = source.search(/server\.keepAliveTimeout\s*=/); + const returnIdx = source.search(/return server;/); + + assert.ok(factoryIdx !== -1, "expected createServerWithResponsesWs to exist"); + assert.ok(keepAliveIdx !== -1, "expected a server.keepAliveTimeout assignment to exist"); + assert.ok(returnIdx !== -1, "expected the wrapped server to be returned"); + assert.ok( + keepAliveIdx > factoryIdx, + "timeout wiring must happen inside createServerWithResponsesWs" + ); + assert.ok( + keepAliveIdx < returnIdx, + "timeout wiring must happen before the server object is returned to the caller" + ); +}); diff --git a/tests/unit/stream-utilities.test.ts b/tests/unit/stream-utilities.test.ts index 0f3404869c..33a1b2bdd7 100644 --- a/tests/unit/stream-utilities.test.ts +++ b/tests/unit/stream-utilities.test.ts @@ -133,7 +133,16 @@ test("createPassthroughStreamWithLogger synthesizes reasoning summary events fro assert.match(result, /event: response\.output_item\.done/); }); -test("createPassthroughStreamWithLogger shows a placeholder for encrypted reasoning items", async () => { +// Reconciles #7095 (xz-dev — chat clients never saw ANY signal that Codex was +// reasoning when the upstream Responses API exposed only encrypted private +// reasoning) with #7176 (JxnLexn — mutating `item.summary` with a fabricated +// placeholder corrupted the response item forwarded downstream, discarding the +// `encrypted_content` shape Codex needs for follow-up requests). Both goals are +// satisfied simultaneously: the client-facing synthetic delta/part events still +// carry the placeholder text, but the forwarded `response.output_item.done` +// payload is untouched — `encrypted_content` survives and no `summary` is +// fabricated onto the wire item. +test("createPassthroughStreamWithLogger shows a placeholder for encrypted reasoning items without mutating the forwarded item", async () => { const transform = createPassthroughStreamWithLogger( "codex", null, @@ -169,12 +178,77 @@ test("createPassthroughStreamWithLogger shows a placeholder for encrypted reason result += decoder.decode(value); } + // #7095: chat clients still see the placeholder via the synthetic events. assert.match(result, /event: response\.reasoning_summary_text\.delta/); assert.match(result, /Codex is reasoning/); - assert.match(result, /"summary":\[\{"type":"summary_text","text":"Codex is reasoning/); + + // #7176: the forwarded response.output_item.done payload is untouched — + // encrypted_content survives intact and no fabricated `summary` is present. + assert.match(result, /"encrypted_content":"enc_opaque_state"/); + assert.doesNotMatch(result, /"summary":/); assert.match(result, /event: response\.output_item\.done/); }); +// Companion guard for the test above. The output_item.done line is echoed +// verbatim, so a re-introduced `item.summary` mutation would NOT surface there. +// It does surface here: the reasoning item is captured into +// passthroughResponsesOutputItems and re-serialized into the response.completed +// snapshot when upstream sends an empty `output` (store: false). This is the +// path that actually fails if the mutation comes back — it is what makes the +// #7176 half of the reconciliation enforceable rather than incidental. +test("createPassthroughStreamWithLogger backfills completed output with encrypted reasoning unmutated", async () => { + const transform = createPassthroughStreamWithLogger( + "codex", + null, + null, + "gpt-5.5-low", + null, + null, + null, + null, + null, + "openai-responses" + ); + + const writer = transform.writable.getWriter(); + await writer.write( + new TextEncoder().encode( + [ + "event: response.output_item.done", + 'data: {"type":"response.output_item.done","response_id":"resp_reasoning_3","output_index":0,"item":{"id":"rs_resp_reasoning_3_0","type":"reasoning","encrypted_content":"enc_opaque_state"}}', + "", + "event: response.completed", + 'data: {"type":"response.completed","response":{"id":"resp_reasoning_3","model":"gpt-5.5-low","status":"completed","output":[],"usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}}', + "", + ].join("\n") + ) + ); + await writer.close(); + + const reader = transform.readable.getReader(); + const decoder = new TextDecoder(); + let result = ""; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + result += decoder.decode(value); + } + + // The placeholder still reached the client (#7095). + assert.match(result, /event: response\.reasoning_summary_text\.delta/); + assert.match(result, /Codex is reasoning/); + + // The item re-serialized into the completed snapshot carries the original + // encrypted_content and never a fabricated summary (#7176). + const completed = result + .split(/\n\n+/) + .find((block) => block.includes('"type":"response.completed"')); + assert.ok(completed, "expected a response.completed event on the wire"); + assert.match(completed, /"encrypted_content":"enc_opaque_state"/); + assert.doesNotMatch(completed, /"summary":/); +}); + test("createPassthroughStreamWithLogger backfills completed output from function_call arguments events", async () => { const transform = createPassthroughStreamWithLogger( "codex", diff --git a/tests/unit/streaming-empty-content-block-1382.test.ts b/tests/unit/streaming-empty-content-block-1382.test.ts new file mode 100644 index 0000000000..750d7d673a --- /dev/null +++ b/tests/unit/streaming-empty-content-block-1382.test.ts @@ -0,0 +1,138 @@ +/** + * Issue #1382 (upstream decolua/9router) — a streaming Claude response that + * opens a `content_block_start` (type "text", initial text "") and then + * immediately `content_block_stop`s WITHOUT ever emitting a + * `content_block_delta` carrying real text/tool_use content must be treated + * as an empty/malformed response, not a valid completion. + * + * Before this fix, `validateResponseQuality`'s bounded SSE peek stopped + * buffering (and reported `valid: true`) as soon as ANY content_block_* + * event was observed — including a content_block_start whose block never + * carries usable text. Tool-heavy requests against backends that mishandle + * tool definitions (reported: DeepSeek, GLM via claude→openai translation) + * can emit exactly this shape: a lifecycle that "completes" successfully at + * the transport layer while the client receives no usable content. The + * combo loop never saw this as a failure, so no failover to the next model + * in the combo ever happened. + */ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { validateResponseQuality } = await import("../../open-sse/services/combo.ts"); + +const encoder = new TextEncoder(); +const silentLog = { warn: () => {} }; + +function claudeSseStream(events: string[]): ReadableStream { + const body = events.join("\n") + "\n"; + return new ReadableStream({ + start(controller) { + controller.enqueue(encoder.encode(body)); + controller.close(); + }, + }); +} + +/** + * Build a mock Claude 200 streaming response with a content_block_start/stop + * pair carrying EMPTY text and no tool_use block — the shape reported in + * #1382 for tool-heavy claude→openai requests against DeepSeek/GLM. + */ +function makeEmptyTextBlockStream(): Response { + const events = [ + `event: message_start\ndata: ${JSON.stringify({ + type: "message_start", + message: { + id: "msg_test_1382", + type: "message", + role: "assistant", + model: "deepseek-v4-pro-max", + content: [], + stop_reason: null, + stop_sequence: null, + usage: { input_tokens: 19882, output_tokens: 0 }, + }, + })}`, + "", + `event: content_block_start\ndata: ${JSON.stringify({ + type: "content_block_start", + index: 0, + content_block: { type: "text", text: "" }, + })}`, + "", + `event: content_block_stop\ndata: ${JSON.stringify({ + type: "content_block_stop", + index: 0, + })}`, + "", + `event: message_delta\ndata: ${JSON.stringify({ + type: "message_delta", + delta: { stop_reason: "end_turn", stop_sequence: null }, + usage: { input_tokens: 0, output_tokens: 25 }, + })}`, + "", + `event: message_stop\ndata: ${JSON.stringify({ type: "message_stop" })}`, + "", + ]; + + return new Response(claudeSseStream(events), { + status: 200, + headers: { "content-type": "text/event-stream" }, + }); +} + +test("#1382 streaming Claude response with empty content_block (no text, no tool_use) is marked invalid", async () => { + const res = makeEmptyTextBlockStream(); + const out = await validateResponseQuality(res, true, silentLog); + assert.equal( + out.valid, + false, + `expected invalid for empty content_block stream, got valid=true (reason: ${out.reason})` + ); + assert.match(out.reason ?? "", /empty/i, `reason should mention 'empty', got: "${out.reason}"`); +}); + +test("#1382 streaming Claude response with a real tool_use content_block_start remains valid", async () => { + const events = [ + `event: message_start\ndata: ${JSON.stringify({ + type: "message_start", + message: { + id: "msg_test_1382_tool", + type: "message", + role: "assistant", + model: "deepseek-v4-pro-max", + content: [], + stop_reason: null, + stop_sequence: null, + usage: { input_tokens: 100, output_tokens: 0 }, + }, + })}`, + "", + `event: content_block_start\ndata: ${JSON.stringify({ + type: "content_block_start", + index: 0, + content_block: { type: "tool_use", id: "toolu_1", name: "Bash", input: {} }, + })}`, + "", + `event: content_block_stop\ndata: ${JSON.stringify({ + type: "content_block_stop", + index: 0, + })}`, + "", + `event: message_delta\ndata: ${JSON.stringify({ + type: "message_delta", + delta: { stop_reason: "tool_use", stop_sequence: null }, + usage: { input_tokens: 0, output_tokens: 12 }, + })}`, + "", + `event: message_stop\ndata: ${JSON.stringify({ type: "message_stop" })}`, + "", + ]; + const res = new Response(claudeSseStream(events), { + status: 200, + headers: { "content-type": "text/event-stream" }, + }); + const out = await validateResponseQuality(res, true, silentLog); + assert.equal(out.valid, true, `expected valid for tool_use stream, got invalid: ${out.reason}`); + assert.ok(out.clonedResponse, "clonedResponse must be present for valid streaming response"); +}); diff --git a/tests/unit/sync-next-cycle.test.ts b/tests/unit/sync-next-cycle.test.ts index 8fdbb8b429..fb5ecfdf15 100644 --- a/tests/unit/sync-next-cycle.test.ts +++ b/tests/unit/sync-next-cycle.test.ts @@ -123,3 +123,31 @@ test("i18n resync also propagates the FINALIZED [prevVersion] section into the m "syncs the shipped (finalized) section — without this all 42 mirrors keep it as TBD" ); }); + +// WS0.3 (v3.8.49 quality plan): the captain's sync-back push is the one write path +// with NO CI gate — the merged tree must pass release-green --quick BEFORE the push, +// or the whole PR queue inherits a red tip (G1). --skip-green-gate is the documented +// emergency escape hatch (pre-existing tip reds verified by hand). + +test("greenGateArgs returns the quick release-green command by default", async () => { + const { greenGateArgs } = await import("../../scripts/release/sync-next-cycle.mjs"); + assert.deepEqual(greenGateArgs(["node", "script", "3.8.49"]), [ + "scripts/quality/validate-release-green.mjs", + "--quick", + ]); +}); + +test("greenGateArgs returns null only with the explicit --skip-green-gate flag", async () => { + const { greenGateArgs } = await import("../../scripts/release/sync-next-cycle.mjs"); + assert.equal(greenGateArgs(["node", "script", "3.8.49", "--skip-green-gate"]), null); + assert.notEqual(greenGateArgs(["node", "script", "3.8.49", "--other"]), null); +}); + +test("sync-next-cycle gates the push on release-green (source guard)", () => { + const src = readFileSync(SCRIPT_PATH, "utf8"); + const mainIdx = src.indexOf("function main()"); + const gateCallIdx = src.indexOf("greenGateArgs(process.argv)", mainIdx); + const pushIdx = src.indexOf('git(["push", "origin", BRANCH]'); + assert.ok(gateCallIdx > mainIdx, "main() must call greenGateArgs(process.argv)"); + assert.ok(pushIdx > gateCallIdx, "the release-green gate must run BEFORE the push"); +}); diff --git a/tests/unit/t25-provider-validation-modelid-fallback.test.ts b/tests/unit/t25-provider-validation-modelid-fallback.test.ts index 7912d30d96..03b1f68c0b 100644 --- a/tests/unit/t25-provider-validation-modelid-fallback.test.ts +++ b/tests/unit/t25-provider-validation-modelid-fallback.test.ts @@ -114,3 +114,50 @@ test("T25: fallback chat probe treats 429 as valid credentials with warning", as globalThis.fetch = originalFetch; } }); + +// decolua/9router#2032: OpenAI-compatible "Check" silently passed for ANY +// non-empty Model ID because a chat-probe 404 (model_not_found) fell through +// the generic "4xx other than auth" branch with no warning. The user only +// discovered the bad model id after a real request tripped the per-model +// lockout. A 404 must surface a warning at Check time instead of a bare pass. +test("T25 / #2032: fallback chat probe surfaces a warning on 404 model_not_found", async () => { + const originalFetch = globalThis.fetch; + + globalThis.fetch = async (url) => { + if (String(url).endsWith("/models")) { + return new Response(JSON.stringify({ error: "Not Found" }), { status: 404 }); + } + return new Response( + JSON.stringify({ + error: { + message: "The model glm-5.2 does not exist.", + type: "invalid_request_error", + param: null, + code: "model_not_found", + }, + }), + { status: 404 } + ); + }; + + try { + const result = await validateProviderApiKey({ + provider: "openai-compatible-chat-t25-model-not-found", + apiKey: "sk-test", + providerSpecificData: { + baseUrl: "https://api.example.com/v1", + validationModelId: "glm-5.2", + }, + }); + + // Credentials themselves are fine (404 is not an auth failure), so this + // still resolves as valid — but MUST carry an actionable warning instead + // of a silent pass, so the user learns about the bad model id at Check + // time rather than after the first real request gets locked out. + assert.equal(result.valid, true); + assert.equal(result.method, "inference_available"); + assert.match(result.warning, /model.*(?:not found|does not exist|glm-5\.2)/i); + } finally { + globalThis.fetch = originalFetch; + } +}); diff --git a/tests/unit/ui/CliAgentsPage.test.tsx b/tests/unit/ui/CliAgentsPage.test.tsx index 82f4a5205f..257f5e4149 100644 --- a/tests/unit/ui/CliAgentsPage.test.tsx +++ b/tests/unit/ui/CliAgentsPage.test.tsx @@ -54,12 +54,18 @@ const { default: CliAgentsPageClient } = await import( // ── Fixtures ────────────────────────────────────────────────────────────────── -/** 6 agent tool ids from the catalog (§3.2 of plan-14) */ +/** + * Agent tool ids from the catalog (§3.2 of plan-14, category: "agent" in + * src/shared/constants/cliTools.ts). "omp" and "letta" were added to the + * catalog after plan-14 shipped, bringing the count from 6 to 8. + */ const AGENT_IDS = [ "openclaw", "hermes-agent", "goose", "interpreter", + "omp", + "letta", "warp", "agent-deck", ] as const; @@ -144,9 +150,9 @@ describe("CliAgentsPageClient", () => { expect(container.textContent).toContain("pageTitle"); }, 15000); - it("2. renders exactly 6 agent tool cards", async () => { + it("2. renders exactly 8 agent tool cards", async () => { const container = await renderPage(); - expect(countAgentCards(container)).toBe(6); + expect(countAgentCards(container)).toBe(8); }, 15000); it("3. search filter — 'hermes' shows 1 card (hermes-agent)", async () => { diff --git a/tests/unit/ui/HermesAgentToolCard-model-aliases.test.tsx b/tests/unit/ui/HermesAgentToolCard-model-aliases.test.tsx new file mode 100644 index 0000000000..5d58e13640 --- /dev/null +++ b/tests/unit/ui/HermesAgentToolCard-model-aliases.test.tsx @@ -0,0 +1,46 @@ +import { describe, expect, it } from "vitest"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, resolve } from "node:path"; + +// Regression probe for issue #7151: OpenRouter (and every other +// `passthroughModels` provider — requesty, dgrid, agentrouter, charm-hyper, +// etc.) never appears in the Hermes Agent role model picker. +// +// Root cause: derives a passthrough provider's model list +// from the `modelAliases` prop (ModelSelectModal.tsx groupedModels → +// buildPassthroughAliasModels(modelAliases, providerId)). When `modelAliases` +// is `{}` (the component default), that helper returns `[]` and the provider +// group is skipped entirely — see modelSelectModalHelpers.ts. Every sibling +// CLI tool card (Codex, Claude, Cline, Kilo, Droid, OpenClaw, Antigravity) +// fetches `/api/models/alias` and passes the result through, but +// HermesAgentToolCard never does, so OpenRouter's managed-available-model +// aliases (synced automatically after the connection is tested — see +// syncManagedAvailableModelAliases in src/lib/providerModels/managedAvailableModels.ts) +// are invisible to it. + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const CARD_PATH = resolve( + __dirname, + "../../../src/app/(dashboard)/dashboard/cli-code/components/HermesAgentToolCard.tsx" +); + +describe("HermesAgentToolCard model alias wiring (#7151)", () => { + const source = readFileSync(CARD_PATH, "utf8"); + + it("declares modelAliases state", () => { + expect(source).toMatch(/const \[modelAliases, setModelAliases\] = useState\(\{\}\)/); + }); + + it("fetches /api/models/alias when expanded", () => { + expect(source).toContain('fetch("/api/models/alias")'); + }); + + it("passes modelAliases prop to ModelSelectModal", () => { + // Regression guard: this prop is what unlocks passthrough provider groups + // (OpenRouter, Requesty, DGrid, AgentRouter, Charm Hyper, ...) in the + // Hermes Agent role picker. Without it, OpenRouter is silently absent + // from the "Select" modal for every role (Default, Delegation, ...). + expect(source).toMatch(/modelAliases=\{modelAliases\}/); + }); +}); diff --git a/tests/unit/ui/agent-bridge-server-card-a11y.test.tsx b/tests/unit/ui/agent-bridge-server-card-a11y.test.tsx index 9db239e614..b1892179c6 100644 --- a/tests/unit/ui/agent-bridge-server-card-a11y.test.tsx +++ b/tests/unit/ui/agent-bridge-server-card-a11y.test.tsx @@ -2,7 +2,7 @@ * a11y tests for AgentBridgeServerCard — each action button must have aria-label. * Uses source-text inspection (no JSDOM render needed) for the structural assertion. */ -import { describe, it } from "node:test"; +import { describe, it } from "vitest"; import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; diff --git a/tests/unit/ui/cli-tools-no-mitm-tab.test.tsx b/tests/unit/ui/cli-tools-no-mitm-tab.test.tsx index bce2f28fcc..cba7c99ffe 100644 --- a/tests/unit/ui/cli-tools-no-mitm-tab.test.tsx +++ b/tests/unit/ui/cli-tools-no-mitm-tab.test.tsx @@ -4,7 +4,7 @@ * * Uses source-text inspection — no JSDOM render needed. */ -import { describe, it } from "node:test"; +import { describe, it } from "vitest"; import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; diff --git a/tests/unit/ui/compressionHub-context-editing.test.tsx b/tests/unit/ui/compressionHub-context-editing.test.tsx index 88fee2e490..6abeb667b6 100644 --- a/tests/unit/ui/compressionHub-context-editing.test.tsx +++ b/tests/unit/ui/compressionHub-context-editing.test.tsx @@ -139,8 +139,11 @@ describe("CompressionHub — Context Editing", () => { }); await flush(); + // CompressionHub deliberately does NOT use useTranslations (see the + // hydration note at the top of CompressionHub.tsx) — its strings are + // literal English text, exactly like EngineConfigPage. const text = container.textContent ?? ""; - expect(text).toContain("Compressão delegada ao provedor"); + expect(text).toContain("Provider-delegated compression"); expect(text).toContain("Context Editing (Claude)"); }); @@ -156,8 +159,8 @@ describe("CompressionHub — Context Editing", () => { await flush(); const text = container.textContent ?? ""; - expect(text).toContain("apenas para Claude"); - expect(text).toContain("não reescrevemos a mensagem"); + expect(text).toContain("available for Claude (Anthropic) only"); + expect(text).toContain("we do not rewrite the message"); }); it("PUTs contextEditing: { enabled: true } when the toggle is flipped on", async () => { diff --git a/tests/unit/ui/compressionHub.test.tsx b/tests/unit/ui/compressionHub.test.tsx index 166c0fef04..27d380d3f0 100644 --- a/tests/unit/ui/compressionHub.test.tsx +++ b/tests/unit/ui/compressionHub.test.tsx @@ -111,28 +111,13 @@ async function flush() { // ── Tests ───────────────────────────────────────────────────────────────── describe("CompressionHub", () => { - it("renders the master switch, mode selector, and the layered pipeline", async () => { - setupFetchMock({ enabled: true, mode: "stacked", pipeline: [{ engine: "rtk" }] }); - const { default: CompressionHub } = - await import("../../../src/app/(dashboard)/dashboard/context/combos/CompressionHub"); - - let container!: HTMLElement; - await act(async () => { - container = mountInContainer(); - }); - await flush(); - - const text = container.textContent ?? ""; - expect(text).toContain("Compression Hub"); - expect(text).toContain("Token Saver"); - expect(text).toContain("Stacked"); - // Active pipeline engine (from the default combo) renders - expect(text).toContain("RTK"); - // Inactive engines from the catalog render too - expect(text).toContain("Caveman"); - // Active-pipeline callout shows when enabled && stacked - expect(text).toContain("Layer pipeline is active"); - }); + // NOTE: the master Token Saver toggle, mode selector, and layered-pipeline + // preview this describe block used to assert on were removed by the Phase 2 + // Hub redesign (see the "Phase 2" comment at the top of CompressionHub.tsx — + // the Hub is now a thin overview with just an active-profile selector + the + // Context Editing toggle). That redesign, including the explicit assertion + // that the master toggle/mode selector/reorder buttons no longer render, is + // covered by compressionHub-active-selector.test.tsx. it("INVARIANT #1: no per-layer control issues a PUT/POST to /api/context/combos/default", { timeout: 20000 }, async () => { const comboWrites: { method: string }[] = []; @@ -186,22 +171,6 @@ describe("CompressionHub", () => { expect(comboWrites).toHaveLength(0); }); - - it("shows the activation warning when Token Saver is off", async () => { - setupFetchMock({ enabled: false, mode: "off", pipeline: [] }); - const { default: CompressionHub } = - await import("../../../src/app/(dashboard)/dashboard/context/combos/CompressionHub"); - - let container!: HTMLElement; - await act(async () => { - container = mountInContainer(); - }); - await flush(); - - const text = container.textContent ?? ""; - expect(text).toContain("Enable Token Saver"); - expect(text).toContain("only run in Stacked mode"); - }); }); describe("CompressionCombosPageClient", () => { diff --git a/tests/unit/ui/conversation-tab-separators.test.tsx b/tests/unit/ui/conversation-tab-separators.test.tsx index 7faad9b6b1..5723955a56 100644 --- a/tests/unit/ui/conversation-tab-separators.test.tsx +++ b/tests/unit/ui/conversation-tab-separators.test.tsx @@ -3,7 +3,7 @@ * Validates the rendering logic: both sections appear when both request/response have turns; * only CONTEXT HISTORY appears when response is empty. */ -import { describe, it } from "node:test"; +import { describe, it } from "vitest"; import assert from "node:assert/strict"; import { normalizeConversation } from "../../../src/mitm/inspector/conversationNormalizer.ts"; import type { InterceptedRequest } from "../../../src/mitm/inspector/types.ts"; diff --git a/tests/unit/ui/conversation-tab.test.tsx b/tests/unit/ui/conversation-tab.test.tsx index 62d405258e..92f8ab213c 100644 --- a/tests/unit/ui/conversation-tab.test.tsx +++ b/tests/unit/ui/conversation-tab.test.tsx @@ -1,7 +1,7 @@ /** * Tests for ConversationTab — normalizeConversation + chat bubble rendering logic */ -import { describe, it } from "node:test"; +import { describe, it } from "vitest"; import assert from "node:assert/strict"; import { normalizeConversation } from "../../../src/mitm/inspector/conversationNormalizer.ts"; import type { InterceptedRequest } from "../../../src/mitm/inspector/types.ts"; diff --git a/tests/unit/ui/historic-session-banner.test.tsx b/tests/unit/ui/historic-session-banner.test.tsx index 910178514d..fb27927710 100644 --- a/tests/unit/ui/historic-session-banner.test.tsx +++ b/tests/unit/ui/historic-session-banner.test.tsx @@ -1,7 +1,7 @@ /** * Tests for HistoricSessionBanner — render with sessionName/null + backToLive callback */ -import { describe, it } from "node:test"; +import { describe, it } from "vitest"; import assert from "node:assert/strict"; // Pure logic tests — no DOM needed (no next-intl in node:test runner) diff --git a/tests/unit/ui/home-topology-hidden-4596.test.tsx b/tests/unit/ui/home-topology-hidden-4596.test.tsx index cdb3a64662..2c3ba169b6 100644 --- a/tests/unit/ui/home-topology-hidden-4596.test.tsx +++ b/tests/unit/ui/home-topology-hidden-4596.test.tsx @@ -19,6 +19,14 @@ describe("home topology hidden networking (#4596)", () => { globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean } ).IS_REACT_ACT_ENVIRONMENT = true; websocketMock.mockClear(); + // useLiveDashboard runs a runtime handshake (GET /api/v1/ws?handshake=1) to + // discover the public WS URL before it opens the socket, so it never + // connects to the hardcoded default and then flaps to the public URL. Stub + // it to fail fast instead of letting the real global fetch hit the network. + vi.stubGlobal( + "fetch", + vi.fn(() => Promise.reject(new Error("network unavailable in test"))) + ); vi.stubGlobal( "WebSocket", class WebSocketMock { @@ -59,9 +67,14 @@ describe("home topology hidden networking (#4596)", () => { expect(websocketMock).not.toHaveBeenCalled(); }); - it("opens a WebSocket when the topology section is enabled", () => { - act(() => { + it("opens a WebSocket when the topology section is enabled", async () => { + await act(async () => { root!.render(); + // Let the handshake fetch's rejection propagate through .catch()/.finally() + // so `wsUrlResolved` flips to true and the connect effect re-runs. + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); }); expect(websocketMock).toHaveBeenCalledTimes(1); }); diff --git a/tests/unit/ui/memories-tab.test.tsx b/tests/unit/ui/memories-tab.test.tsx index cb56eaf143..845e1e4d7e 100644 --- a/tests/unit/ui/memories-tab.test.tsx +++ b/tests/unit/ui/memories-tab.test.tsx @@ -292,18 +292,27 @@ describe("MemoriesTab", () => { }); it("calls DELETE when delete confirmed", async () => { - const mockFetch = vi.fn(); - mockFetch.mockResolvedValueOnce({ - ok: true, - json: async () => ({ - data: MOCK_MEMORIES, - total: 2, - totalPages: 1, - stats: { total: 2, tokensUsed: 0, hitRate: 0, cacheStats: { hits: 0, misses: 0 } }, - }), + // MemoriesTab fires two independent fetches on mount: an immediate health + // check (/api/memory/health) and a 300ms-debounced memories list fetch + // (/api/memory?...). A call-order-dependent mock (mockResolvedValueOnce + + // fallback) is fragile here because the health check resolves first and + // would consume the "once" response meant for the list. Key off the URL + // instead, like the rest of this file's fetch mocks do. + const mockFetch = vi.fn((url: string) => { + if (typeof url === "string" && url.startsWith("/api/memory?")) { + return Promise.resolve({ + ok: true, + json: async () => ({ + data: MOCK_MEMORIES, + total: 2, + totalPages: 1, + stats: { total: 2, tokensUsed: 0, hitRate: 0, cacheStats: { hits: 0, misses: 0 } }, + }), + }); + } + return Promise.resolve({ ok: true, json: async () => ({}) }); }); - mockFetch.mockResolvedValue({ ok: true, json: async () => ({}) }); - globalThis.fetch = mockFetch; + globalThis.fetch = mockFetch as unknown as typeof fetch; const { default: MemoriesTab } = await import( "../../../src/app/(dashboard)/dashboard/memory/components/tabs/MemoriesTab" ); diff --git a/tests/unit/ui/model-select-modal-hidden-models-7156.test.tsx b/tests/unit/ui/model-select-modal-hidden-models-7156.test.tsx new file mode 100644 index 0000000000..957f033a7f --- /dev/null +++ b/tests/unit/ui/model-select-modal-hidden-models-7156.test.tsx @@ -0,0 +1,68 @@ +// @vitest-environment jsdom +import React, { act } from "react"; +import { createRoot } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import ModelSelectModal from "@/shared/components/ModelSelectModal"; + +vi.mock("next-intl", () => ({ + useTranslations: () => (key: string) => key, +})); + +const roots: Array<{ root: ReturnType; el: HTMLDivElement }> = []; + +async function render(props: React.ComponentProps): Promise { + const el = document.createElement("div"); + document.body.appendChild(el); + const root = createRoot(el); + await act(async () => { + root.render(); + }); + roots.push({ root, el }); + return el; +} + +beforeEach(() => { + (globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; + vi.stubGlobal( + "fetch", + vi.fn(async (input: RequestInfo | URL) => { + const url = String(input); + if (url.includes("/api/combos")) return new Response(JSON.stringify({ combos: [] }), { status: 200 }); + if (url.includes("/api/provider-nodes")) return new Response(JSON.stringify({ nodes: [] }), { status: 200 }); + if (url.includes("/api/provider-models")) { + return new Response( + JSON.stringify({ + models: { + requesty: [ + { id: "visible-model-1", name: "Visible Model", source: "imported" }, + { id: "hidden-model-1", name: "Hidden Model", source: "imported", isHidden: true }, + ], + }, + modelCompatOverrides: [], + }), + { status: 200 } + ); + } + return new Response(JSON.stringify({}), { status: 200 }); + }) + ); +}); + +afterEach(() => { + for (const { root, el } of roots.splice(0)) { act(() => root.unmount()); el.remove(); } + vi.unstubAllGlobals(); + vi.clearAllMocks(); +}); + +describe("ModelSelectModal hidden-model filtering (#7156)", () => { + it("does not list a custom model explicitly flagged isHidden:true", async () => { + const el = await render({ + isOpen: true, onClose: vi.fn(), onSelect: vi.fn(), + activeProviders: [{ provider: "requesty", id: "conn-1" }], + modelAliases: {}, title: "Add model to combo", + }); + await act(async () => { await new Promise((r) => setTimeout(r, 0)); }); + expect(el.textContent).toContain("Visible Model"); + expect(el.textContent).not.toContain("Hidden Model"); + }); +}); diff --git a/tests/unit/ui/playground-build-tab.test.tsx b/tests/unit/ui/playground-build-tab.test.tsx index c954679818..2699aa4c53 100644 --- a/tests/unit/ui/playground-build-tab.test.tsx +++ b/tests/unit/ui/playground-build-tab.test.tsx @@ -64,6 +64,52 @@ function renderBuildTab(config = BASE_CONFIG): HTMLDivElement { return el; } +// ── BuildWizard navigation helpers ────────────────────────────────────────── +// +// BuildTab now renders behind a 3-step wizard (BuildWizard.tsx): +// step 1 — pick a mode (Tools / JSON / Both) +// step 2 — configure tools and/or the JSON schema, depending on the mode +// step 3 — run + toolbar badges + prompt textarea +// +// next-intl is mocked as a key pass-through above, so every translated label +// renders as its raw i18n key (e.g. "nextButton", "modeToolsTitle"). + +type BuildMode = "tools" | "json" | "both"; + +function clickNext(el: HTMLDivElement): void { + const nextBtn = Array.from(el.querySelectorAll("button")).find((b) => + b.textContent?.includes("nextButton"), + ) as HTMLButtonElement; + act(() => { + nextBtn.click(); + }); +} + +function selectMode(el: HTMLDivElement, mode: BuildMode): void { + // Step 1's default mode is already "tools" — only click a mode card when a + // different mode is required. + if (mode === "tools") return; + const key = mode === "json" ? "modeJsonTitle" : "modeBothTitle"; + const card = Array.from(el.querySelectorAll("button")).find((b) => + b.textContent?.includes(key), + ) as HTMLButtonElement; + act(() => { + card.click(); + }); +} + +/** Drive the wizard from step 1 to step 2, selecting `mode` along the way. */ +function goToStep2(el: HTMLDivElement, mode: BuildMode = "tools"): void { + selectMode(el, mode); + clickNext(el); +} + +/** Drive the wizard from step 1 all the way to step 3 (run screen). */ +function goToStep3(el: HTMLDivElement, mode: BuildMode = "tools"): void { + goToStep2(el, mode); + clickNext(el); +} + afterEach(() => { for (const { root, el } of containers) { act(() => root.unmount()); @@ -76,28 +122,34 @@ afterEach(() => { describe("BuildTab", () => { it("renders Run button", () => { const el = renderBuildTab(); - const runBtn = el.querySelector("[class*='bg-primary']"); - expect(runBtn?.textContent).toContain("runLabel"); + goToStep3(el); + const runBtn = Array.from(el.querySelectorAll("button")).find((b) => + b.textContent?.includes("runButton"), + ); + expect(runBtn).not.toBeUndefined(); }); it("renders Function calling section", () => { const el = renderBuildTab(); - expect(el.textContent).toContain("Function calling"); + goToStep2(el, "tools"); + expect(el.textContent).toContain("toolsLabel"); expect(el.textContent).toContain("Add tool"); }); it("renders Structured output section", () => { const el = renderBuildTab(); - expect(el.textContent).toContain("Structured output"); + goToStep2(el, "json"); + expect(el.textContent).toContain("structuredOutputLabel"); expect(el.textContent).toContain("JSON mode"); }); it("adds a tool and shows it in function calling UI", async () => { const el = renderBuildTab(); + goToStep2(el, "tools"); - // Find add tool form inputs in the right panel + // Find add tool form inputs in the tools panel const allInputs = el.querySelectorAll("input[type='text']") as NodeListOf; - // The first or second input should be the function name + // The first input is the function name const nameInput = allInputs[0]; act(() => setInputValue(nameInput, "search_web")); @@ -115,16 +167,15 @@ describe("BuildTab", () => { it("shows validation error for invalid JSON in tool params", async () => { const el = renderBuildTab(); + goToStep2(el, "tools"); const allInputs = el.querySelectorAll("input[type='text']") as NodeListOf; act(() => setInputValue(allInputs[0], "bad_tool")); // The parameters textarea is in the Add tool form section — it has default valid JSON. - // We need to find the textarea labeled "JSON schema for parameters" in the add form. const paramsTextareas = Array.from(el.querySelectorAll("textarea")).filter( (t) => t.getAttribute("aria-label") === "JSON schema for parameters", ); - // The last one is in the Add tool form (the first may be the message prompt textarea) const paramsTextarea = paramsTextareas[paramsTextareas.length - 1] as HTMLTextAreaElement; act(() => setInputValue(paramsTextarea, "NOT JSON {{{")); @@ -140,6 +191,7 @@ describe("BuildTab", () => { it("enables JSON mode toggle and shows schema editor", async () => { const el = renderBuildTab(); + goToStep2(el, "json"); const toggle = el.querySelector("[role='switch']") as HTMLButtonElement; expect(toggle).not.toBeNull(); @@ -153,6 +205,7 @@ describe("BuildTab", () => { it("shows tool badge in toolbar when tools are added", async () => { const el = renderBuildTab(); + goToStep2(el, "tools"); const allInputs = el.querySelectorAll("input[type='text']") as NodeListOf; act(() => setInputValue(allInputs[0], "my_tool")); @@ -163,7 +216,9 @@ describe("BuildTab", () => { ) as HTMLButtonElement; await act(async () => { addToolBtn.click(); }); - // Badge "1 tool" should appear in toolbar + clickNext(el); // step 2 -> step 3 + + // Badge "1 tool" should appear in the step-3 toolbar expect(el.textContent).toContain("1 tool"); }); @@ -183,6 +238,7 @@ describe("BuildTab", () => { ); const el = renderBuildTab(); + goToStep2(el, "tools"); // Add a tool const allInputs = el.querySelectorAll("input[type='text']") as NodeListOf; @@ -193,14 +249,16 @@ describe("BuildTab", () => { ) as HTMLButtonElement; await act(async () => { addToolBtn.click(); }); - // Type a prompt - const promptTextarea = el.querySelector("textarea[placeholder*='message']") as HTMLTextAreaElement; + clickNext(el); // step 2 -> step 3 + + // Type a prompt (the only textarea left on step 3 is the prompt input) + const promptTextarea = el.querySelector("textarea") as HTMLTextAreaElement; act(() => setInputValue(promptTextarea, "Run this tool")); - // Click Run (label is "runLabel" via mocked t()) + // Click Run (label is "runButton" via mocked t()) const runBtns = el.querySelectorAll("button"); const runBtn = Array.from(runBtns).find( - (b) => b.textContent?.includes("runLabel") && !b.textContent?.includes("clearAll"), + (b) => b.textContent?.includes("runButton"), ) as HTMLButtonElement; await act(async () => { runBtn.click(); }); await act(async () => { @@ -218,8 +276,12 @@ describe("BuildTab", () => { it("shows JSON mode badge in toolbar when JSON mode is enabled", async () => { const el = renderBuildTab(); + goToStep2(el, "json"); const toggle = el.querySelector("[role='switch']") as HTMLButtonElement; await act(async () => { toggle.click(); }); + + clickNext(el); // step 2 -> step 3 + expect(el.textContent).toContain("JSON mode"); }); }); diff --git a/tests/unit/ui/playground-compare-tab.test.tsx b/tests/unit/ui/playground-compare-tab.test.tsx index bbb12ebe53..82fc3ad0e8 100644 --- a/tests/unit/ui/playground-compare-tab.test.tsx +++ b/tests/unit/ui/playground-compare-tab.test.tsx @@ -92,9 +92,9 @@ function renderCompareTab(config = BASE_CONFIG): HTMLDivElement { return el; } -function setInputValue(el: HTMLInputElement, value: string) { +function setInputValue(el: HTMLInputElement | HTMLTextAreaElement, value: string) { const nativeSetter = Object.getOwnPropertyDescriptor( - window.HTMLInputElement.prototype, + el instanceof HTMLTextAreaElement ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype, "value", )?.set; nativeSetter?.call(el, value); @@ -204,6 +204,10 @@ describe("CompareTab", () => { act(() => setInputValue(input, "model-2")); await act(async () => { addBtn.click(); }); + // Run all is disabled until a prompt is entered + const promptTextarea = el.querySelector("[aria-label='User prompt']") as HTMLTextAreaElement; + act(() => setInputValue(promptTextarea, "Compare this")); + const runBtn = el.querySelector("[aria-label='Run all columns']") as HTMLButtonElement; await act(async () => { runBtn.click(); }); @@ -219,6 +223,11 @@ describe("CompareTab", () => { ); const el = renderCompareTab(); + + // Run all is disabled until a prompt is entered + const promptTextarea = el.querySelector("[aria-label='User prompt']") as HTMLTextAreaElement; + act(() => setInputValue(promptTextarea, "Compare this")); + const runBtn = el.querySelector("[aria-label='Run all columns']") as HTMLButtonElement; act(() => { runBtn.click(); }); diff --git a/tests/unit/ui/playground-studio.test.tsx b/tests/unit/ui/playground-studio.test.tsx index 8a975219ba..91db18dbf8 100644 --- a/tests/unit/ui/playground-studio.test.tsx +++ b/tests/unit/ui/playground-studio.test.tsx @@ -68,6 +68,10 @@ vi.mock("@/shared/components/MonacoEditor", () => ({ vi.mock("@/shared/constants/providers", () => ({ ALIAS_TO_ID: {}, + AI_PROVIDERS: {}, + OPENAI_COMPATIBLE_PREFIX: "openai-compatible-", + ANTHROPIC_COMPATIBLE_PREFIX: "anthropic-compatible-", + CLAUDE_CODE_COMPATIBLE_PREFIX: "anthropic-compatible-cc-", })); vi.mock("@/shared/utils/maskEmail", () => ({ diff --git a/tests/unit/ui/provider-plan-config.test.tsx b/tests/unit/ui/provider-plan-config.test.tsx deleted file mode 100644 index 0351a92a54..0000000000 --- a/tests/unit/ui/provider-plan-config.test.tsx +++ /dev/null @@ -1,134 +0,0 @@ -// @vitest-environment jsdom -import React from "react"; -import { act } from "react"; -import { createRoot } from "react-dom/client"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -vi.mock("next-intl", () => ({ - useTranslations: () => (key: string) => key, -})); - -vi.mock("@/shared/components", () => ({ - Button: ({ - children, - onClick, - disabled, - }: { - children: React.ReactNode; - onClick?: () => void; - disabled?: boolean; - }) => ( - - ), -})); - -vi.mock("@/shared/components/ProviderIcon", () => ({ - default: () => , -})); - -vi.mock("@/lib/quota/planRegistry", () => ({ - knownProviders: () => ["openai", "anthropic"], - getKnownPlan: (prov: string) => { - if (prov === "openai") { - return { dimensions: [{ unit: "tokens", window: "daily", limit: 100000 }] }; - } - return null; - }, -})); - -const MOCK_CONNECTIONS = [ - { id: "conn_1", provider: "openai", name: "GPT Account" }, - { id: "conn_2", provider: "anthropic", email: "user@example.com" }, -]; - -const mockFetch = vi.fn(); -vi.stubGlobal("fetch", mockFetch); - -const { default: ProviderPlanConfigClient } = await import( - "../../../src/app/(dashboard)/dashboard/costs/quota-share/plans/ProviderPlanConfigClient" -); - -let container: HTMLDivElement | null = null; -let root: ReturnType | null = null; - -async function renderPage() { - (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = - true; - container = document.createElement("div"); - document.body.appendChild(container); - await act(async () => { - root = createRoot(container!); - root.render(); - }); - // Wait for initial fetch effect to resolve - await act(async () => { - await new Promise((r) => setTimeout(r, 30)); - }); -} - -describe("ProviderPlanConfigClient", { timeout: 15000 }, () => { - beforeEach(() => { - mockFetch.mockImplementation((url: string) => { - if (String(url).includes("/api/providers/client")) { - return Promise.resolve({ - ok: true, - json: () => Promise.resolve({ connections: MOCK_CONNECTIONS }), - } as unknown as Response); - } - if (String(url).includes("/api/quota/plans")) { - return Promise.resolve({ - ok: true, - json: () => Promise.resolve([]), - } as unknown as Response); - } - return Promise.resolve({ - ok: true, - json: () => Promise.resolve({}), - } as unknown as Response); - }); - }); - - afterEach(() => { - if (root && container) act(() => root!.unmount()); - container?.remove(); - container = null; - root = null; - vi.clearAllMocks(); - }); - - it("renders the page title", async () => { - await renderPage(); - expect(document.body.innerHTML).toContain("title"); - }); - - it("renders catalog section with known providers", async () => { - await renderPage(); - // catalogTitle key should appear - expect(document.body.innerHTML).toContain("catalogTitle"); - expect(document.body.innerHTML).toContain("openai"); - }); - - it("renders connection selector with options", async () => { - await renderPage(); - const select = document.querySelector("select") as HTMLSelectElement; - expect(select).not.toBeNull(); - expect(select.options.length).toBeGreaterThan(1); - }); - - it("shows right-panel placeholder when no connection selected", async () => { - await renderPage(); - expect(document.body.innerHTML).toContain("unknownProviderNotice"); - }); - - it("renders save button after selecting a connection", async () => { - await renderPage(); - const select = document.querySelector("select") as HTMLSelectElement; - await act(async () => { - select.value = "conn_1"; - select.dispatchEvent(new Event("change", { bubbles: true })); - }); - expect(document.body.innerHTML).toContain("saveOverrideButton"); - }); -}); diff --git a/tests/unit/ui/same-context-filter.test.tsx b/tests/unit/ui/same-context-filter.test.tsx index ddc9aaef0b..40bcc16b57 100644 --- a/tests/unit/ui/same-context-filter.test.tsx +++ b/tests/unit/ui/same-context-filter.test.tsx @@ -6,7 +6,7 @@ * - RequestRow exports an onSameContext prop * - useTrafficFilters.setSameContext is referenced from TrafficInspectorPageClient */ -import { describe, it } from "node:test"; +import { describe, it } from "vitest"; import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; @@ -17,21 +17,35 @@ const ROOT = path.resolve( __dirname, "../../../src/app/(dashboard)/dashboard/tools/traffic-inspector" ); +const SRC_ROOT = path.resolve(__dirname, "../../../src"); function read(rel: string): string { return fs.readFileSync(path.join(ROOT, rel), "utf8"); } +function readSrc(rel: string): string { + return fs.readFileSync(path.join(SRC_ROOT, rel), "utf8"); +} + describe("R5-4 same-context filter end-to-end", () => { it("useTrafficStream.applyFilter has sameContextKey branch", () => { - const src = read("hooks/useTrafficStream.ts"); + // The comparison itself now lives in the extracted, independently-testable + // matchesTrafficFilter() helper (src/lib/inspector/matchesTrafficFilter.ts) — + // useTrafficStream.applyFilter just delegates to it. + const hookSrc = read("hooks/useTrafficStream.ts"); assert.ok( - src.includes("sameContextKey") && src.includes("contextKey"), - "applyFilter should branch on sameContextKey / contextKey" + hookSrc.includes("matchesTrafficFilter"), + "applyFilter should delegate to matchesTrafficFilter" + ); + + const matcherSrc = readSrc("lib/inspector/matchesTrafficFilter.ts"); + assert.ok( + matcherSrc.includes("sameContextKey") && matcherSrc.includes("contextKey"), + "matchesTrafficFilter should branch on sameContextKey / contextKey" ); // Must actually exclude requests where contextKey differs assert.ok( - src.includes("req.contextKey !== f.sameContextKey"), + matcherSrc.includes("req.contextKey !== f.sameContextKey"), "should exclude when contextKey !== sameContextKey" ); }); diff --git a/tests/unit/ui/search-tools-compare-tab.test.tsx b/tests/unit/ui/search-tools-compare-tab.test.tsx index 3e014faf8a..3cb92d0ffc 100644 --- a/tests/unit/ui/search-tools-compare-tab.test.tsx +++ b/tests/unit/ui/search-tools-compare-tab.test.tsx @@ -270,12 +270,13 @@ describe("CompareTab", () => { await new Promise((r) => setTimeout(r, 150)); }); - // Check that the table exists and contains overlap info - const table = el.querySelector("table"); - if (table) { + // The results panel renders as a div-based side-by-side layout (not a ) — + // the overlap summary footer lives inside [data-testid='compare-results']. + const resultsPanel = el.querySelector("[data-testid='compare-results']"); + if (resultsPanel) { // URL overlap row should contain a fraction like "1/2" - const tableText = table.textContent ?? ""; - expect(tableText).toMatch(/URL overlap|\d+\/\d+/); + const panelText = resultsPanel.textContent ?? ""; + expect(panelText).toMatch(/in common|\d+\/\d+/); } else { // Loading state is still active — acceptable expect(el.querySelector("[data-testid='compare-loading']")).toBeTruthy(); diff --git a/tests/unit/ui/search-tools-scrape-tab.test.tsx b/tests/unit/ui/search-tools-scrape-tab.test.tsx index 67509d4f47..f63bac664b 100644 --- a/tests/unit/ui/search-tools-scrape-tab.test.tsx +++ b/tests/unit/ui/search-tools-scrape-tab.test.tsx @@ -112,7 +112,9 @@ describe("ScrapeTab", () => { }); const errorEl = el.querySelector("[data-testid='url-error']"); expect(errorEl).toBeTruthy(); - expect(errorEl?.textContent).toContain("URL"); + // next-intl is mocked as a key pass-through above (per repo convention), so the + // rendered text is the raw i18n key, not the translated "URL is required" copy. + expect(errorEl?.textContent).toContain("scrapeUrlRequired"); }); it("shows error for invalid URL", () => { diff --git a/tests/unit/ui/session-recorder-bar.test.tsx b/tests/unit/ui/session-recorder-bar.test.tsx index 2363617277..d4e88c48e3 100644 --- a/tests/unit/ui/session-recorder-bar.test.tsx +++ b/tests/unit/ui/session-recorder-bar.test.tsx @@ -1,7 +1,7 @@ /** * Tests for SessionRecorderBar — start/stop flow + timer logic */ -import { describe, it } from "node:test"; +import { describe, it } from "vitest"; import assert from "node:assert/strict"; function formatElapsed(s: number): string { diff --git a/tests/unit/ui/stats-tab.test.tsx b/tests/unit/ui/stats-tab.test.tsx index 3cdd100ee8..03703966e0 100644 --- a/tests/unit/ui/stats-tab.test.tsx +++ b/tests/unit/ui/stats-tab.test.tsx @@ -2,7 +2,7 @@ * Asserts that StatsTab lazy-loads StatsCharts via next/dynamic (ssr: false) * and does NOT statically import anything from "recharts". */ -import { describe, it } from "node:test"; +import { describe, it } from "vitest"; import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; diff --git a/tests/unit/ui/timing-i18n.test.tsx b/tests/unit/ui/timing-i18n.test.tsx index 5d2ee14a77..e52f840c82 100644 --- a/tests/unit/ui/timing-i18n.test.tsx +++ b/tests/unit/ui/timing-i18n.test.tsx @@ -5,7 +5,7 @@ * Round-3 F-I18N translated ConversationTab/StatsTab/StatsCharts but missed * TimingTab (5 labels) and TimingWaterfall (2 labels). Round-4 closed the gap. */ -import { describe, it } from "node:test"; +import { describe, it } from "vitest"; import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; diff --git a/tests/unit/ui/traffic-inspector-page.test.tsx b/tests/unit/ui/traffic-inspector-page.test.tsx index f908eab22b..67f1cbd1c0 100644 --- a/tests/unit/ui/traffic-inspector-page.test.tsx +++ b/tests/unit/ui/traffic-inspector-page.test.tsx @@ -1,7 +1,7 @@ /** * Smoke tests for Traffic Inspector page structure and constants */ -import { describe, it } from "node:test"; +import { describe, it } from "vitest"; import assert from "node:assert/strict"; describe("Traffic Inspector page smoke tests", () => { diff --git a/tests/unit/ui/use-resizable-panels.test.tsx b/tests/unit/ui/use-resizable-panels.test.tsx index 156d653738..5dee3cdcf1 100644 --- a/tests/unit/ui/use-resizable-panels.test.tsx +++ b/tests/unit/ui/use-resizable-panels.test.tsx @@ -1,7 +1,7 @@ /** * Tests for useResizablePanels — drag changes width, collapse to 48px, localStorage persistence */ -import { describe, it } from "node:test"; +import { describe, it } from "vitest"; import assert from "node:assert/strict"; const MIN_WIDTH = 280; diff --git a/tests/unit/ui/use-session-recorder.test.tsx b/tests/unit/ui/use-session-recorder.test.tsx index f4889b244a..f39277a653 100644 --- a/tests/unit/ui/use-session-recorder.test.tsx +++ b/tests/unit/ui/use-session-recorder.test.tsx @@ -4,7 +4,7 @@ * Verifies that during recording, new traffic WS events trigger * POST to /api/tools/traffic-inspector/sessions/{id}/requests. */ -import { describe, it, before, after } from "node:test"; +import { describe, it } from "vitest"; import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; diff --git a/tests/unit/ui/use-system-proxy-exit-guard.test.tsx b/tests/unit/ui/use-system-proxy-exit-guard.test.tsx index af1eaeeb1b..fa6d7597a5 100644 --- a/tests/unit/ui/use-system-proxy-exit-guard.test.tsx +++ b/tests/unit/ui/use-system-proxy-exit-guard.test.tsx @@ -6,7 +6,7 @@ * This matches how use-traffic-stream.test.tsx tests hook logic (pure logic, * no React renderer needed). */ -import { describe, it, beforeEach } from "node:test"; +import { describe, it, beforeEach } from "vitest"; import assert from "node:assert/strict"; // --------------------------------------------------------------------------- diff --git a/tests/unit/ui/use-traffic-stream.test.tsx b/tests/unit/ui/use-traffic-stream.test.tsx index ce8804fa9b..39ca601e28 100644 --- a/tests/unit/ui/use-traffic-stream.test.tsx +++ b/tests/unit/ui/use-traffic-stream.test.tsx @@ -1,7 +1,7 @@ /** * Tests for useTrafficStream — WebSocket snapshot/new/update/clear + reconnect backoff */ -import { describe, it, before, after, mock } from "node:test"; +import { describe, it } from "vitest"; import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; diff --git a/tests/unit/ui/use-virtual-list.test.tsx b/tests/unit/ui/use-virtual-list.test.tsx index 5ef5e689fb..bfb1bc9a53 100644 --- a/tests/unit/ui/use-virtual-list.test.tsx +++ b/tests/unit/ui/use-virtual-list.test.tsx @@ -1,7 +1,7 @@ /** * Tests for useVirtualList — virtualizes 1000+ items without rendering all */ -import { describe, it } from "node:test"; +import { describe, it } from "vitest"; import assert from "node:assert/strict"; const ESTIMATED_ROW_HEIGHT = 48; diff --git a/tests/unit/validate-release-green.test.ts b/tests/unit/validate-release-green.test.ts index bf62fd8096..7626975264 100644 --- a/tests/unit/validate-release-green.test.ts +++ b/tests/unit/validate-release-green.test.ts @@ -42,6 +42,20 @@ test("parseCognitiveCount reads the gate's count (en + pt)", () => { assert.equal(parseCognitiveCount("no number"), null); }); +test("parseCognitiveCount ignores the cyclomatic count in the combined ratchets output (#7009)", () => { + // `check:complexity-ratchets` runs ONE shared ESLint walk and prints BOTH ratchets. + // The cyclomatic "N violações" summary is emitted FIRST, so a bare `\\d+ violações` + // regex captured 2056 (cyclomatic) instead of 890 (cognitive) — a phantom drift in + // every pre-flight report. Prefer the unambiguous machine-readable `cognitiveComplexity=N`. + const combined = [ + "complexity=2056", + "cognitiveComplexity=890", + "[complexity] OK — 2056 violações (baseline 2056)", + "[cognitive-complexity] OK — 890 violações (baseline 890)", + ].join("\n"); + assert.equal(parseCognitiveCount(combined), 890); +}); + test("isDrift flags only growth past the committed baseline (down-direction ratchets)", () => { assert.equal(isDrift(3900, 3867), true); // grew → drift assert.equal(isDrift(3867, 3867), false); // equal → ok diff --git a/tests/unit/vercel-deploy-sso-protection-check.test.ts b/tests/unit/vercel-deploy-sso-protection-check.test.ts new file mode 100644 index 0000000000..d3fb945e96 --- /dev/null +++ b/tests/unit/vercel-deploy-sso-protection-check.test.ts @@ -0,0 +1,95 @@ +// Regression guard for upstream report: "Vercel Relay with Codex returns 403 +// Access denied and lacks source diagnostics". +// +// Root cause: the Vercel deploy route disables project SSO/Deployment +// Protection via a PATCH request, but fired it with `.catch(() => {})` and +// never inspected `res.ok`. If Vercel rejects or no-ops the PATCH (plan +// doesn't allow disabling protection, stale/under-scoped token, etc.), the +// relay is still saved and activated as a healthy proxy pool — later +// requests routed through it fail with an undiagnosed `403 Access denied` +// from Vercel's own deployment protection, indistinguishable from an +// upstream-provider 403. +// +// Fix: check the PATCH response and surface the failure back to the caller +// (`ssoProtectionWarning` in the JSON response) instead of silently +// swallowing it, so the UI/API consumer can diagnose the 403 source. +import { describe, it, beforeEach, afterEach } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { __disableSsoProtectionForTest } from "../../src/app/api/settings/proxy/vercel-deploy/route"; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); +const ROUTE_PATH = join( + ROOT, + "src/app/api/settings/proxy/vercel-deploy/route.ts" +); + +describe("disableSsoProtection — checks the Vercel PATCH response instead of swallowing it", () => { + const originalFetch = global.fetch; + + afterEach(() => { + global.fetch = originalFetch; + }); + + it("reports failure when Vercel rejects the PATCH (e.g. plan does not allow disabling protection)", async () => { + global.fetch = (async () => + new Response(JSON.stringify({ error: { message: "Forbidden" } }), { + status: 403, + })) as typeof fetch; + + const result = await __disableSsoProtectionForTest( + "https://api.vercel.com", + "proj_123", + "test-token" + ); + + assert.equal(result.ok, false, "must report ok:false on a non-2xx PATCH response"); + assert.equal(result.status, 403); + }); + + it("reports success when Vercel accepts the PATCH", async () => { + global.fetch = (async () => new Response(null, { status: 200 })) as typeof fetch; + + const result = await __disableSsoProtectionForTest( + "https://api.vercel.com", + "proj_123", + "test-token" + ); + + assert.equal(result.ok, true); + }); + + it("reports failure (not a thrown exception) when the PATCH request itself fails", async () => { + global.fetch = (async () => { + throw new Error("network down"); + }) as typeof fetch; + + const result = await __disableSsoProtectionForTest( + "https://api.vercel.com", + "proj_123", + "test-token" + ); + + assert.equal(result.ok, false); + }); +}); + +describe("vercel-deploy route — wires the SSO-protection check into the response", () => { + const src = readFileSync(ROUTE_PATH, "utf8"); + + it("no longer fires the PATCH with a silent `.catch(() => {})`", () => { + assert.ok( + !/ssoProtection:\s*null[\s\S]*?\.catch\(\s*\(\)\s*=>\s*\{\s*\}\s*\)/.test(src), + "the ssoProtection PATCH must not be silently swallowed with .catch(() => {})" + ); + }); + + it("surfaces a warning in the JSON response when disabling SSO protection failed", () => { + assert.ok( + src.includes("ssoProtectionWarning"), + "POST handler must surface ssoProtectionWarning in the response payload when the PATCH failed" + ); + }); +}); diff --git a/tests/unit/verify-published.test.ts b/tests/unit/verify-published.test.ts new file mode 100644 index 0000000000..b4a9be09df --- /dev/null +++ b/tests/unit/verify-published.test.ts @@ -0,0 +1,40 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { + parseVersionArg, + buildDockerArgs, + CONTAINER_SCRIPT, +} from "../../scripts/release/verify-published.mjs"; + +// WS1.4 (v3.8.49 quality plan) — pure-function guards for the post-publish verifier +// (clean-container install of the PUBLISHED bytes + boot). The end-to-end path is +// exercised live against the registry; these pin the safety-relevant logic. + +test("parseVersionArg accepts strict semver incl. prerelease", () => { + assert.equal(parseVersionArg("3.8.48"), "3.8.48"); + assert.equal(parseVersionArg("3.9.0-rc.1"), "3.9.0-rc.1"); +}); + +test("parseVersionArg rejects shell-hostile and malformed input", () => { + for (const bad of ["", "3.8", "latest", "3.8.48; rm -rf /", "$(whoami)", "3.8.48 && x"]) { + assert.equal(parseVersionArg(bad), null, `should reject: ${bad}`); + } +}); + +test("buildDockerArgs passes the version via env, never into the script body", () => { + const args = buildDockerArgs("3.8.48"); + assert.equal(args[0], "run"); + assert.ok(args.includes("VERIFY_VERSION=3.8.48"), "version must travel as -e env"); + const script = args[args.length - 1]; + assert.equal(script, CONTAINER_SCRIPT); + assert.ok(!script.includes("3.8.48"), "script body must not embed the version (Hard Rule #13)"); + assert.ok(args.includes("node:24-slim"), "clean base image"); + assert.ok(args.includes("--rm"), "container must not linger"); +}); + +test("container script installs from the registry and polls health with a version match", () => { + assert.ok(CONTAINER_SCRIPT.includes('npm install -g "omniroute@${VERIFY_VERSION}"')); + assert.ok(CONTAINER_SCRIPT.includes("/api/monitoring/health")); + assert.ok(CONTAINER_SCRIPT.includes("body.version === want"), "must assert the served version"); + assert.ok(CONTAINER_SCRIPT.includes("WRONG VERSION"), "must fail loudly on version mismatch"); +}); diff --git a/tests/unit/web-cookie-validation-proxy-7058.test.ts b/tests/unit/web-cookie-validation-proxy-7058.test.ts new file mode 100644 index 0000000000..9c573020bc --- /dev/null +++ b/tests/unit/web-cookie-validation-proxy-7058.test.ts @@ -0,0 +1,98 @@ +// Regression test for #7058 — zai-web (and every other entry-bearing web-cookie +// provider) never honored a configured HTTP/SOCKS proxy during connection-test / +// cookie validation. +// +// Root cause: validateWebCookieProvider() probed `${baseUrl}/models` via +// directHttpsRequest(), which hardcodes `bypassProxyPatch: true` — forcing +// safeOutboundFetch to use the pre-patch native fetch and skip proxy-context/ +// env-var resolution entirely. That bypass was introduced in #3226 as a narrow, +// documented exception for a single NVIDIA NIM workaround +// (see tests/unit/proxy-bypass-scope-guard-3226.test.ts) but validateWebCookieProvider +// adopted it as its default transport from inception (#4023), silently extending the +// bypass to every web-cookie provider with a registry entry (zai-web among them). +// +// This test proves the cookie-validation probe reaches a local forward proxy +// (via a real CONNECT tunnel — the same mechanism undici uses for both HTTP and +// HTTPS targets) when one is configured via HTTP_PROXY, exactly like the +// specialty web-cookie validators (chatgpt-web, grok-web, ...) already do via +// validationRead/validationWrite. +import test from "node:test"; +import assert from "node:assert/strict"; +import http from "node:http"; +import net from "node:net"; + +const { validateWebCookieProvider } = await import("../../src/lib/providers/validation.ts"); +const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts"); +const { clearDispatcherCache } = await import("../../open-sse/utils/proxyDispatcher.ts"); + +const zaiWebEntry = REGISTRY["zai-web"] as { baseUrl?: string } | undefined; +const ORIGINAL_BASE_URL = zaiWebEntry?.baseUrl; +const ORIGINAL_HTTP_PROXY = process.env.HTTP_PROXY; + +test.after(() => { + if (zaiWebEntry && ORIGINAL_BASE_URL !== undefined) { + zaiWebEntry.baseUrl = ORIGINAL_BASE_URL; + } + if (ORIGINAL_HTTP_PROXY === undefined) { + delete process.env.HTTP_PROXY; + } else { + process.env.HTTP_PROXY = ORIGINAL_HTTP_PROXY; + } + clearDispatcherCache(); +}); + +test("zai-web cookie validation routes through the configured HTTP_PROXY (#7058)", async () => { + assert.ok(zaiWebEntry, "zai-web must have a providerRegistry entry for this test to be meaningful"); + + // Stand-in for chat.z.ai's /models probe target. + const target = http.createServer((_req, res) => { + res.writeHead(200, { "content-type": "application/json" }); + res.end("{}"); + }); + await new Promise((resolve) => target.listen(0, () => resolve())); + const targetPort = (target.address() as net.AddressInfo).port; + + // Minimal forward proxy that only speaks CONNECT (like a real corporate proxy) and + // always tunnels to the local target above, regardless of the requested host — this + // lets the "upstream" host be a non-resolvable placeholder without any real DNS + // dependency, while still proving the request actually reached the proxy. + let sawConnect = false; + const proxy = http.createServer((_req, res) => { + res.writeHead(501); + res.end("CONNECT only"); + }); + proxy.on("connect", (_req, socket) => { + sawConnect = true; + const upstream = net.connect(targetPort, "127.0.0.1", () => { + socket.write("HTTP/1.1 200 Connection Established\r\n\r\n"); + upstream.pipe(socket); + socket.pipe(upstream); + }); + upstream.on("error", () => socket.destroy()); + socket.on("error", () => upstream.destroy()); + }); + await new Promise((resolve) => proxy.listen(0, () => resolve())); + const proxyPort = (proxy.address() as net.AddressInfo).port; + + // A non-local-looking hostname: isLocalAddress()/resolveProxyForRequest() force a + // direct connection for any 127.*/localhost/LAN target, which would defeat this test. + zaiWebEntry!.baseUrl = "http://zai-web-validation-probe-7058.invalid"; + process.env.HTTP_PROXY = `http://127.0.0.1:${proxyPort}`; + clearDispatcherCache(); + + try { + const result = await validateWebCookieProvider({ provider: "zai-web", apiKey: "token=fake" }); + + assert.equal( + sawConnect, + true, + "BUG #7058: zai-web cookie validation never reached the configured HTTP_PROXY " + + "(bypassProxyPatch:true unconditionally uses the native, unpatched fetch)" + ); + assert.equal(result.valid, true, `expected a valid session, got ${JSON.stringify(result)}`); + } finally { + target.close(); + proxy.close(); + clearDispatcherCache(); + } +}); diff --git a/tsconfig.typecheck-dashboard.json b/tsconfig.typecheck-dashboard.json new file mode 100644 index 0000000000..a6cfaddf7c --- /dev/null +++ b/tsconfig.typecheck-dashboard.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "incremental": false + }, + "include": ["src/app/(dashboard)/**/*.ts", "src/app/(dashboard)/**/*.tsx"] +} diff --git a/vitest.config.ts b/vitest.config.ts index 4664608302..6f8d643da5 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -6,6 +6,7 @@ export default defineConfig({ test: { environment: "jsdom", globals: true, + setupFiles: ["./tests/_setup/vitestUiPolyfills.ts"], pool: "threads", maxWorkers: 20, fileParallelism: true,