v3.8.14
diegosouzapw/OmniRoutev3.8.14Jun 7, 2026by diegosouzapw
AI Summary
Adds per-provider custom headers and hardens security against SSRF attacks. The release also fixes translator bugs for Gemini/Vertex tools and improves Electron process termination.
Key Highlights
- Added per-provider custom headers for OpenAI/Anthropic-compatible nodes.
- Fixed critical SSRF vulnerability in provider auto-sync self-fetch.
- Fixed Gemini/Vertex tool parameter types to OBJECT.
- Fixed Electron process tree termination on Exit/Update.
- Fixed proxy auto-selection to be opt-in.
New Features
- Per-provider custom headers
- SSRF security fix
- Tool parameter type coercion
- Process tree kill
- Opt-in proxy selection
Full Release Notes
### β¨ New Features
- **feat(api):** per-provider **custom headers** for OpenAI/Anthropic-compatible provider nodes β attach operator-defined headers (e.g. tenant/routing headers) to upstream requests via a new `customHeaders` field on provider nodes (`custom_headers_json` column, migration 095). Hardened on merge: values/names validated through the canonical `upstreamHeadersRecordSchema` (CRLF/control-char/length/16-max) with a single shared `isForbiddenCustomHeaderName()` denylist (hop-by-hop + auth), applied case-insensitively, and honored for `anthropic-compatible-cc-*` nodes too. ([#3338](https://github.com/diegosouzapw/OmniRoute/pull/3338) β thanks @pizzav-xyz / @diegosouzapw)
### π Security
- **fix(security):** provider auto-sync self-fetch now uses a trusted loopback/env-pinned origin (`getModelSyncInternalBaseUrl()`) instead of `new URL(request.url).origin`, so a management-authenticated caller can no longer redirect the credential-bearing internal request to an arbitrary host via the `Host` header (CodeQL `js/request-forgery`, critical). Shipped to Docker/Electron in v3.8.13; reaches npm here (npm `3.8.13` was immutable). ([#3336](https://github.com/diegosouzapw/OmniRoute/pull/3336), CodeQL #323 β thanks @diegosouzapw)
### π§ Bug Fixes
- **fix(translator):** every Gemini/Vertex `functionDeclaration.parameters` is now coerced to an OBJECT-typed schema before cleaning. Clients like GitHub Copilot send some tools (e.g. `terminal_last_command`) whose `parameters` is present but lacks a top-level `type: "object"` (just `{ properties }`, a scalar type, or `{}`); these slipped through `buildGeminiTools`' `params || default` guard and Vertex rejected them with `[400] ... functionDeclaration parameters schema should be of type OBJECT`. Hardens every OpenAIβGemini tool request (Vertex / antigravity / agy / gemini). (#3357 β thanks @nullbytef0x)
- **fix(gemini):** normalize Gemini/Antigravity textual `[Tool call: ...]` markers β stop suppressing **false positives** (legitimate assistant prose that merely mentions `[Tool call: terminal]`, e.g. in backticks, is preserved instead of being swallowed) and correctly buffer markers **split across streaming chunks** (`[Tool` + ` call: terminal]` + `Arguments: {...}`), flushing the text when it turns out not to be a tool call. Dedups the parsing/validation into a shared `open-sse/utils/textualToolCall.ts` (new `isValidToolCallHeaderPrefix`) and adds `gemini-2.5-flash`/`gemini-3.5-flash-low` model specs. ([#3358](https://github.com/diegosouzapw/OmniRoute/pull/3358) β thanks @Ardem2025 / @diegosouzapw)
- **fix(electron):** clicking "Exit" (or applying an update) now terminates the **whole** server process tree, not just the direct child. The embedded server runs as `omniroute.exe`-as-node (`ELECTRON_RUN_AS_NODE`) and spawns grandchildren (embedded services, MITM proxy, tunnels); on Windows `ChildProcess.kill()` only terminates the direct child, so survivors kept `omniroute.exe` locked β the process "hung in memory" after Exit and updates failed with "file in use". New `killProcessTree()` helper uses `taskkill /PID <pid> /T /F` on Windows (signal-based on POSIX); wired into `stopNextServer`, the `waitForServerExit` force-kill, and `installUpdate`. (#3347 β thanks @Flexible78)
- **fix(proxy):** proxy auto-selection is now **opt-in** (new `PROXY_AUTO_SELECT_ENABLED` flag, default off). Previously a single proxy in the registry silently became a global fallback for **all** provider connections (the Step-11 fallback listed every registry proxy, ignoring assignments and per-connection `proxy_enabled`). It now no-ops unless the operator enables the flag. (#3332 β thanks @hertznsk)
- **fix(cli):** write the OpenCode config to `~/.config/opencode/opencode.json` on **all** platforms β on Windows OmniRoute wrote to `%APPDATA%\opencode\` but OpenCode reads from `%USERPROFILE%\.config\opencode\` (XDG), so dashboard-saved config silently had no effect. (#3330 β thanks @abdulkadirozyurt)
- **fix(catalog):** remove `minimaxai/minimax-m3` from the **NVIDIA NIM** tier β NVIDIA does not host it yet, so every request 404'd (`404 page not found`), while sibling `minimax-m2.7` on the same provider works. MiniMax M3 stays available on the tiers that actually serve it. (#3329 β thanks @mikmaneggahommie)
- **fix(sse):** treat **MiniMax M3** as multimodal so the compression layer no longer strips image parts from vision requests β `lite.ts modelSupportsVision` now keeps images for `minimax-m3*` (see also the registry `supportsVision` alignment in Maintenance). ([#3328](https://github.com/diegosouzapw/OmniRoute/pull/3328) β thanks @diegosouzapw)
- **fix(oauth):** Kiro **Builder ID** token import no longer fails with "Bad credentials" β `validateImportToken` only ever tried the social-auth refresh; it now uses the cached AWS SSO `clientId`/`clientSecret` (`~/.aws/sso/cache/*.json`) and the OIDC refresh path (`authMethod: "builder-id"`), with a TDD harness. ([#3333](https://github.com/diegosouzapw/OmniRoute/pull/3333) β thanks @quanturbo / @diegosouzapw)
- **fix(provider-proxy):** honor per-account proxy toggles β a connection with `proxy_enabled = false` is no longer forced through an assigned/registry proxy. ([#3349](https://github.com/diegosouzapw/OmniRoute/pull/3349) β thanks @rdself)
- **fix(providers):** reduce proxy label noise on the provider page (clearer proxy assignment/state display). ([#3346](https://github.com/diegosouzapw/OmniRoute/pull/3346) β thanks @wilsonicdev)
- **fix(noauth):** expose only **usable** model aliases for no-auth providers, so the catalog no longer advertises aliases that can't actually be called. ([#3345](https://github.com/diegosouzapw/OmniRoute/pull/3345) β thanks @oyi77)
- **fix(duckduckgo):** restore the bare `Response` contract for the DuckDuckGo/browser-backed executor (rebased onto the cycle), fixing a wrapping-contract regression. ([#3323](https://github.com/diegosouzapw/OmniRoute/pull/3323) β thanks @oyi77 / @diegosouzapw)
- **fix(dashboard):** drop the duplicate "Distribute Proxies" button on the provider page β it rendered twice at once (provider toolbar + accounts-list header) whenever connections existed and none were selected. The toolbar button (global) and the per-tag-group buttons remain. ([#3352](https://github.com/diegosouzapw/OmniRoute/pull/3352) β thanks @diegosouzapw)
- **fix(electron):** ship `loginManager.js` in the packaged app β #3292 added it (and a `require("./loginManager")` in `main.js`) without adding it to electron-builder's `build.files`, so the packaged app crashed at startup with "Cannot find module" on the Linux/macOS smoke tests. Plus a regression test asserting every local `require("./x")` in the Electron entry points is shipped. ([#3334](https://github.com/diegosouzapw/OmniRoute/pull/3334) β thanks @diegosouzapw)
- **fix(startup):** correct the #3292 auto-refresh daemon import (`@/open-sse/...` β `@omniroute/open-sse/services/autoRefreshDaemon`); the `@/` alias maps to `src/`, so the daemon silently never ran in the built standalone (non-fatal "Cannot find module", caught at runtime). Adds a regression test banning `@/open-sse/*` imports in `src/`. ([#3335](https://github.com/diegosouzapw/OmniRoute/pull/3335) β thanks @diegosouzapw)
- **fix(electron):** wrap `autoUpdater.checkForUpdates()` so a 404/offline/rate-limited update check can no longer surface as an unhandled rejection (the `error` event still notifies the user); fixes the macOS-intel packaged-app smoke failure. ([#3339](https://github.com/diegosouzapw/OmniRoute/pull/3339) β thanks @diegosouzapw)
- **fix(dashboard):** stop the infinite render loop on `/dashboard/cli-agents/hermes-agent` β `HermesAgentToolCard` listed `currentRoles` in the config-load effect's deps while `loadCurrentConfig()` set `currentRoles` to a fresh object on every fetch, so the effect re-fired β refetched β re-set forever (the page spun and spammed `GET /api/cli-tools/hermes-agent-settings` in the console; it manifested only on the always-expanded detail page). `loadCurrentConfig` is now memoized and the batch-seed reads `currentRoles` via a functional update, so the effect runs once. Adds a jsdom regression test asserting the settings endpoint is fetched a bounded number of times. ([#3353](https://github.com/diegosouzapw/OmniRoute/pull/3353) β thanks @diegosouzapw)
- **fix(dashboard):** the Usage Analytics card now surfaces the **real** backend error (status + message) instead of a generic placeholder when `/api/usage/analytics` fails β a new shared `fetchError.ts` helper extracts a useful message. ([#3356](https://github.com/diegosouzapw/OmniRoute/pull/3356) β thanks @diegosouzapw)
### π Maintenance
- **fix(review):** harden the per-provider custom-headers feature surfaced by the `/review-reviews` battery β `updateProviderNode` no longer wipes stored `custom_headers_json` on a partial update that omits the field; `customHeadersSchema` reuses the canonical `upstreamHeadersRecordSchema` guards (CRLF/control-char/length/16-max) and rejects auth header names via a single shared `isForbiddenCustomHeaderName()` denylist (executor + schema no longer keep divergent copies); custom headers now reach the wire for `anthropic-compatible-cc-*` nodes and override the executor's own `Content-Type`/`Accept` case-insensitively instead of duplicating them; and `rowToCamel` normalizes a NULL `_json` column to `baseKey: null`. ([#3350](https://github.com/diegosouzapw/OmniRoute/pull/3350) β thanks @diegosouzapw)
- **fix(catalog):** flag every `minimax-m3` registry entry `supportsVision` (not just the opencode free tier) so the vision-bridge guardrail and the compression layer agree the model is multimodal on all tiers (completes #3328). (thanks @diegosouzapw)
- **fix(oauth):** Kiro Builder ID import forwards the requested `region` to the OIDC validation refresh (no longer pinned to `us-east-1`), prefers the region-matching cached SSO client registration over the first file found, and falls `expiresIn` back to 3600 on the OIDC path. (thanks @diegosouzapw)
- **fix(db):** migration `095` gains an `isSchemaAlreadyApplied` guard so a fresh DB (where `SCHEMA_SQL` already creates `custom_headers_json`) skips it cleanly instead of throwing-then-catching a duplicate-column error. (thanks @diegosouzapw)
- **test:** align stale cycle tests with shipped behavior β NVIDIA `minimaxai/minimax-m3` removal (#3329), the 29th feature flag (`PROXY_AUTO_SELECT_ENABLED`, #3332), and the OpenCode `~/.config` path on Windows (#3330). (thanks @diegosouzapw)
- **docs:** add a documentation comment to the exported `GET` handler in the context-analytics route. ([#3337](https://github.com/diegosouzapw/OmniRoute/pull/3337) β thanks @Lang-Qiu)
- **docs(i18n):** translate 25 core documentation files to Indonesian. ([#3348](https://github.com/diegosouzapw/OmniRoute/pull/3348) β thanks @KrisnaSantosa15)
### π Contributors
Thanks to everyone whose work landed in v3.8.14:
| Contributor | PRs / Issues |
| --- | --- |
| [@pizzav-xyz](https://github.com/pizzav-xyz) | #3338 |
| [@quanturbo](https://github.com/quanturbo) | #3333 |
| [@oyi77](https://github.com/oyi77) | #3323, #3345 |
| [@rdself](https://github.com/rdself) | #3349 |
| [@wilsonicdev](https://github.com/wilsonicdev) | #3346 |
| [@hertznsk](https://github.com/hertznsk) | #3332 |
| [@abdulkadirozyurt](https://github.com/abdulkadirozyurt) | #3330 |
| [@mikmaneggahommie](https://github.com/mikmaneggahommie) | #3329 |
| [@Flexible78](https://github.com/Flexible78) | #3347 |
| [@Lang-Qiu](https://github.com/Lang-Qiu) | #3337 |
| [@KrisnaSantosa15](https://github.com/KrisnaSantosa15) | #3348 |
| [@nullbytef0x](https://github.com/nullbytef0x) | #3357 |
| [@Ardem2025](https://github.com/Ardem2025) | #3358 |
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer β #3334, #3335, #3336, #3339, #3350, #3352, #3353, #3356; review/hardening across the cycle |
---
## What's Changed
* Release v3.8.14 by @diegosouzapw in https://github.com/diegosouzapw/OmniRoute/pull/3340
* docs(changelog): complete v3.8.14 (#3356 + @nullbytef0x/@Ardem2025 credits) by @diegosouzapw in https://github.com/diegosouzapw/OmniRoute/pull/3362
* fix(ci): drop explicit any pushing chatCore over the t11 any-budget by @diegosouzapw in https://github.com/diegosouzapw/OmniRoute/pull/3364
* test(translator): align gemini-2.5-flash maxOutputTokens cap to 65536 (#3358 drift) by @diegosouzapw in https://github.com/diegosouzapw/OmniRoute/pull/3367
**Full Changelog**: https://github.com/diegosouzapw/OmniRoute/compare/v3.8.13...v3.8.14