* chore(release): open v3.8.13 development cycle
Bump 3.8.12 → 3.8.13 across package.json, lockfile, electron/, open-sse/, and
docs/reference/openapi.yaml; add the [3.8.13] cycle placeholder to the root
CHANGELOG and the 41 i18n mirrors. Integration branch for the v3.8.13 cycle —
fixes/features land here via per-issue PRs and it merges to main at release time.
* fix(ci): skip auto-deploy when VPS host is unreachable from the runner (#3299)
Integrated into release/v3.8.13
* fix(dev): auto-rebuild better-sqlite3 on Node ABI mismatch at dev startup (#3301)
Integrated into release/v3.8.13
* feat(api): accept path-scoped API keys on client API routes (#3300)
Integrated into release/v3.8.13
* fix(sse): harden against empty responses causing Copilot Chat failures (#3297)
Integrated into release/v3.8.13
* fix(api): remove Completions.me rickroll provider (discussion #3293) (#3302)
Integrated into release/v3.8.13
* fix(opencode-provider): extract contextLength from live model catalog (#3298)
Integrated into release/v3.8.13
* feat(web-cookie): self-service login infrastructure + auto-refresh daemon (#3292)
Integrated into release/v3.8.13
* docs(changelog): record the v3.8.13 PRs merged this round (#3292/#3300/#3297/#3298/#3301/#3302/#3299)
* fix(auth): harden URL token extraction — drop query-string fallback, gate to client routes (security follow-up to #3300) (#3309)
Security follow-up to #3300 — integrated into release/v3.8.13
* docs: rename resolve-issues → review-issues skill references
* fix(dashboard): keep no-auth providers visible under 'Show configured only' (#3290) (#3312)
no-auth providers (opencode, duckduckgo-web, theoldllm, veoaifree-web) never
create a DB connection row so stats.total stays 0, which the configured-only
filter treated as 'unconfigured' and hid them — even though they are always
usable and appear unconditionally in /v1/models. filterConfiguredProviderEntries
now treats displayAuthType === 'no-auth' as configured.
Co-authored-by: uniQta <uniQta@users.noreply.github.com>
* fix(cli): resolve update paths relative to script + recursive backup (#3295) (#3313)
omniroute update always failed on a global install:
- getCurrentVersion() read package.json from process.cwd(), which on a global
npm/brew install is the user's working dir, not the package root → null →
'Could not determine current version'.
- createBackup() resolved bin/ from cwd too, and passed the 'cli' directory to
copyFileSync → EISDIR, swallowed by the catch → 'Failed to create backup'.
Both now resolve package.json/bin relative to the script via import.meta.url,
and the backup uses cpSync({recursive:true}) so the cli/ directory is copied.
Co-authored-by: uniQta <uniQta@users.noreply.github.com>
* fix(theoldllm): read upstream body once to avoid [502] body-already-read (#3296) (#3314)
On the cached-token path the executor never enters the refresh branch, so the
same upstream Response was read with .text() twice (token-rejection check +
final body). A Response body is single-use, so the second read threw
'Body is unusable: Body has already been read', caught and surfaced as [502].
Read the body once into finalBody and only re-read after a token-rejection
refetch.
Co-authored-by: onizukashonan14-png <onizukashonan14-png@users.noreply.github.com>
* fix(sse): strip leaked internal tool envelopes from streaming output (#3311)
Integrated into release/v3.8.13
* fix(sse): expose Claude + Gemini budget tiers in the antigravity catalog (#3184) (#3303)
Integrated into release/v3.8.13 (#3184)
* fix(catalog): compute combo context_length from known targets only (#3304)
Integrated into release/v3.8.13 — live contextLength + known-targets combo context (#3298 follow-up)
* chore(i18n): add message keys for proxy UI + vscode/ollama endpoint (#3307)
Integrated into release/v3.8.13 — i18n message keys for proxy UI + vscode/ollama
* feat(dashboard): i18n the proxy settings UI (#3310)
Integrated into release/v3.8.13 — i18n the proxy settings UI
* feat(api): model catalog enrichment + MCP model-catalog tools (#3306)
Integrated into release/v3.8.13 — model catalog enrichment + MCP model-catalog tools, reconciled with #3309 URL-token hardening
* test(catalog): align Antigravity preview-alias test with #3303 budget tiers
#3303 added the Gemini `-high`/`-low` budget tiers to ANTIGRAVITY_PUBLIC_MODELS
(user-callable on the Antigravity OAuth backend, verified via #3184), but did
not update the catalog-route test that asserted `antigravity/gemini-3.1-pro-high`
must NOT be exposed. The assertion now reflects the intended behavior — the
client-visible budget alias IS surfaced — while keeping the legacy
`gemini-claude-*` alias keys unexposed. Caught running the full catalog suite
on the merged release HEAD (the #3303 round only ran the antigravity-aliases
and usage-hardening files).
* docs(changelog): record the 6 PRs merged this review round into v3.8.13
#3306/#3307/#3310 (New Features — VS Code split: catalog+MCP, i18n keys, proxy
UI i18n), #3311/#3303/#3304 (Bug Fixes — SSE envelope sanitizer, antigravity
budget tiers, combo known-targets context_length).
* chore(release): finalize v3.8.13 changelog and cleanup
Finalize the v3.8.13 changelog with release date, maintenance notes,
and contributor credits. Update MCP docs to reference the correct tool
inventory diagram, exclude nested .claude worktrees from ESLint scans,
and tighten a response sanitizer type guard.
* fix(dashboard): refresh connections after provider auth import (#3320)
Integrated into release/v3.8.13 — refresh connections after provider auth import
* fix(codex): strip client-only params on native /responses passthrough (#3317) (#3325)
A /v1/responses request against the built-in codex/ provider does an
openai-responses -> openai-responses passthrough (CodexExecutor.transformRequest
returns the body early for _nativeCodexPassthrough). It forwarded client-only
fields verbatim and the Codex upstream rejected them with 400 Unsupported
parameter: prompt_cache_retention / safety_identifier / user — breaking Factory
Droid (which injects all three). The chat-completions path already strips these
(base.ts #1884, openai-responses translator #2770) but the passthrough skips
translation. Strip the three fields in the shared block before the passthrough
return; user is removed unconditionally since Codex /responses always rejects it.
Co-authored-by: tycronk20 <tycronk20@users.noreply.github.com>
* fix(dashboard): normalize agent-bridge /state response to stop page crash (#3318) (#3326)
The Agent Bridge page seeded a well-shaped initialData default then replaced it
wholesale with the raw /api/tools/agent-bridge/state response. The route returns
{ server, agents } but the UI reads { serverState, agentStates, bypassPatterns,
mappings }, so serverState became undefined and AgentBridgeServerCard crashed on
serverState.running — surfaced as the full-page 'Internal Server Error' boundary
(client render error, not a real 5xx).
Add a shared normalizeAgentBridgeState() that maps the route shape into the page
contract (server.running/certExists -> serverState) and always returns safe
defaults (never undefined serverState). Wired into both the SSR loader (page.tsx)
and the polling hook. The legacy 'agents' entry shape differs from AgentStateEntry
so it is not coerced; full route<->page contract reconciliation (port, upstreamCa,
bypassPatterns, mappings, agentStates) is a follow-up.
Co-authored-by: tycronk20 <tycronk20@users.noreply.github.com>
* docs: VS Code/Ollama endpoints + env & i18n tooling (#3319)
Integrated into release/v3.8.13 — VS Code/Ollama docs + env & i18n tooling
* feat(provider): test-all endpoint, rate-limit overrides, visibility f… (#3267)
Integrated into release/v3.8.13 — provider test-all endpoint, rate-limit overrides, model visibility
* feat: auto-combo optimization, playground model dropdown, only-configured toggle (#3322)
Integrated into release/v3.8.13 — auto-combo candidate expansion + playground dropdown + only-configured toggle
* feat(api): VS Code Copilot Ollama-compatible BYOK endpoint (#3316)
Integrated into release/v3.8.13 — VS Code Copilot Ollama-compatible BYOK endpoint (reconciled with #3306/#3309 auth hardening)
* chore(release): document #3320 in the v3.8.13 changelog + contributor credits
---------
Co-authored-by: Felipe Almeman <4226997+zhiru@users.noreply.github.com>
Co-authored-by: Wilson <pedbookmed@gmail.com>
Co-authored-by: Hernan Javier Ardila Sanchez <hjasgr@gmail.com>
Co-authored-by: Paijo <14921983+oyi77@users.noreply.github.com>
Co-authored-by: uniQta <uniQta@users.noreply.github.com>
Co-authored-by: onizukashonan14-png <onizukashonan14-png@users.noreply.github.com>
Co-authored-by: tycronk20 <tycronk20@users.noreply.github.com>
Co-authored-by: Vinayrnani <vinayrnani@gmail.com>
- chipotle/grokTls: explicit null checks instead of a Promise in a boolean
conditional (behavior-preserving; clears the 2 MAJOR reliability bugs)
- sqliteQuotaStore.poolUsage: drop the unreachable dimMap scan loops (dimMap
was never populated) — the lightweight snapshot already returns no
dimensions; poolUsageWithDimensions() is the plan-aware path
- BudgetTab: presentation role + keyboard handler on the checkbox wrapper
test:coverage now enforces 60/60/60/60 (statements/lines/functions/branches);
real coverage is ~75-82% so this tightens the floor without new test work.
Updates the c8 --check-coverage thresholds in package.json and the matching
references in CLAUDE.md (Quick Start, testing table, Copilot policy, Hard
Rule #9). Salvaged from the never-pushed chore/skills-governance-tdd-vps
branch; the i18n CLAUDE.md mirrors carry a separate pre-existing drift and
are not gated by check-docs-sync.
Integrated into release/v3.8.12. Salvaged the emitHookBlocking payload-chaining fix from the now-closed plugins-v4 branch (#3221) and adapted it to the shipped release hooks.ts: each blocking handler now sees the body/metadata as mutated by previous handlers. TDD regression test included (RED before, GREEN after); existing plugins-hooks suites green (19+5), typecheck + lint clean.
- Add the official Telegram group (t.me/omnirouteOficial) and gather Discord,
Telegram and both WhatsApp groups into one community card block at the top;
remove the scattered WhatsApp links from the nav line and the Support section
(now a pointer to the top).
- Move the Free-Token Budget section from the bottom (before License) up to a
hero section near the top, retitled '💰 ~1.9B Free Tokens / Month'.
Integrated into release/v3.8.12. CodeQL hardening on the Chipotle executor: Math.random → crypto.randomInt/randomUUID, and a strict URL hostname check in the test. Fixed the node:crypto import (crypto.randomInt is not on the Web Crypto global → would crash at WS-connect) and added a regression guard exercising both helpers.
The #3247 fix shipped via #3283 (parallel session) 46s after @wilsonicdev
filed the same fix in #3282, leaving his PR stranded with no credit — the
#3242 credit-theft pattern. Repoint the entry to the merged #3283, credit
@wilsonicdev as co-author for the independent diagnosis, and note #3283
refined it to keep rejecting on an explicit-auth-signal 500.
- Regenerate the README/dashboard mockup from the catalog: 28 pools in the grid
(was 9), a balance-floored stacked bar (Mistral now ~40% of the bar, was ~90%),
and a first-month signup-credit strip (~586M). Add the data-driven generator.
- FREE_TIERS.md: drop the alarming '🚫 Avoid / terms prohibit' framing — relabel
those 19 providers as 'caution — worth checking', note their access is real and
the OAuth/keyless ones aren't token-quantifiable (so out of the headline, not
excluded as unusable).
A working Qoder PAT was reported as "expired". The validator probes the Cosy endpoint
(api1.qoder.sh) — which IS the correct PAT path (the executor falls back to it after the
expected 401 from api.qoder.com). The bug was the verdict: isCosyAppError (added by #2860)
marked ANY Cosy 500 with "success":false as an auth failure, including a generic
{..."msgCode":500,"message":"Internal Server Error"} server fault — contradicting the
older #1391 "5xx = valid bypass" rule.
Narrow it: a Cosy 500 only marks the PAT invalid when the body carries an EXPLICIT auth
signal (unauthorized/forbidden/expired/token invalid/...); a generic Internal Server Error
falls back to valid-bypass. #2860's protection for genuine auth rejections is preserved.
Regression test: tests/unit/qoder-cli.test.ts — the two pre-existing generic-500 cases now
assert valid:true (they encoded the #3247 bug) + a new explicit-auth-signal case asserts
valid:false. 13/13 green.
Follow-up to the #3269 private-webhook opt-in. With the opt-in on, the private-host
check was bypassed entirely, leaving cloud-metadata endpoints (169.254.169.254,
metadata.google.internal, 100.100.100.200, link-local 169.254.0.0/16) reachable — the
classic SSRF -> IAM-credential pivot — and the webhook test endpoint returned the
upstream body, making it a content-exfiltration primitive against internal services.
- outboundUrlGuard: add isCloudMetadataHost(); parseAndValidateWebhookUrl blocks those
hosts UNCONDITIONALLY, even when private targets are opted in.
- webhooks/[id]/test: redact responseBody for private targets (status + latency only).
Regression test: tests/unit/webhook-metadata-guard-3269.test.ts (RED before, GREEN after);
existing webhook SSRF/opt-in suites stay green (34/34).
Webhooks hardcoded parseAndValidatePublicUrl, which blocks any RFC1918/loopback host —
breaking self-hosted setups that legitimately point webhooks at internal services
(n8n, Home Assistant, a LAN box). Provider URLs already had an opt-in
(OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS); webhooks now reuse it.
- outboundUrlGuard: add parseAndValidateWebhookUrl — gates the private-host check on
arePrivateProviderUrlsAllowed() (default OFF); protocol + embedded-credential checks
stay unconditional.
- swap all webhook call sites (create/update/test/validate-url + dispatcher x2) to it.
Regression test: tests/unit/webhook-private-optin-3269.test.ts (RED before, GREEN after);
existing webhook SSRF/dispatcher suites stay green (33/33).
Audited every commit since v3.8.11 one-by-one. Added the missing v3.8.12
entries (features #3250/#3259/#3263/#3271, fixes #3248/#3249/#3261/#3256/#3274,
maintenance #3270), repointed the combo-rewrite and web-tools entries to their
actually-merged PRs (#3268, #3275) instead of the closed #3242/issue links, and
added the v3.8.12 Contributors hall. Also co-credited @ibanunmangun on the
v3.8.11 #3203 OAuth fix (independent first diagnosis via #3193).
A custom OpenAI-compatible image-edit provider received an empty `model`. In production
`globalThis.fetch` is patched with node_modules/undici's fetch, whose `FormData` class
differs from `globalThis.FormData`; passing a native FormData made undici serialize it as
the string "[object FormData]" (text/plain), dropping every field including `model`.
handleOpenAIImageEdit now assembles the multipart body as a Buffer with an explicit
boundary + Content-Type, which every fetch impl accepts verbatim.
Regression test: tests/unit/image-edits-multipart-3273.test.ts reproduces the exact prod
condition (routes through undici's fetch) — RED before (upstream got text/plain
[object FormData]), GREEN after. Existing image suites stay green (50/50).
Regression of #764. Claude Code → Groq (llama-3.3-70b-versatile) returned HTTP 400
because the model was treated as reasoning-capable: supportsReasoning() defaulted to true,
so applyThinkingBudget did not strip reasoning params, and the claude→openai translator
forwarded reasoning_effort (and re-injected it from output_config.effort) — which Groq
rejects on non-reasoning models.
- providerRegistry: mark llama-3.3-70b-versatile + llama-4-scout supportsReasoning:false
(gpt-oss / qwen3-32b keep reasoning — they accept reasoning_effort).
- stripThinkingConfig: also strip output_config.effort so the translator can't re-inject
reasoning_effort downstream.
Regression test: tests/unit/thinking-budget-groq-3258.test.ts (RED before, GREEN after);
existing thinking-budget suites stay green (45/45).
ds-web/deepseek-v4-pro emits tool calls wrapped as
<tool_call name="skill">{"name":"customize-opencode"}</tool_call> instead of the
canonical <tool>{json}</tool>. webTools.ts only matched <tool>...</tool>, so the block
was silently dropped (and when arguments were present, the surrounding tag leaked into
content). Add TOOL_CALL_TAG_RE to capture the JSON body — the real tool name comes from
the body, never the tag's name= attribute — and extend the early-exit + range stripping.
Regression test: tests/unit/web-tools-translation-3260.test.ts (RED before, GREEN after).
Existing web-tools suites stay green (26/26).
Bump 3.8.11 → 3.8.12 across package.json, lockfile, electron/, open-sse/, and
docs/reference/openapi.yaml; add the [3.8.12] cycle placeholder to the root
CHANGELOG and the 41 i18n mirrors. Integration branch for the v3.8.12 cycle —
fixes/features land here via per-issue PRs and it merges to main at release time.
CodeQL flagged 7 high alerts in code the cycle touched (the large release-PR diff
re-surfaces them). Resolved at the source — no dismissals:
- fix(images): resolveImageBaseUrl trimmed trailing slashes with `/\/+$/`, a
polynomial-ReDoS pattern (js/polynomial-redos) on the configured node base URL.
Replace it with a non-backtracking endsWith/slice loop.
- test(oauth): pin the Anthropic OAuth host with exact-equality asserts and a
parsed-hostname negative check instead of substring `.includes()`
(js/incomplete-url-substring-sanitization). The exact-equality assertions were
already present, so coverage is unchanged.
- test(images): drop the redundant `!includes("generativelanguage.googleapis.com")`
assert — the exact-equality assert on the resolved URL already guarantees it.
Clear the two release-gate failures the CI Lint+Build jobs surfaced (release-branch
drift — PR merges bypassed pre-push):
- fix(api): /v1/images/edits parsed request.json() without a Zod guard
(route-validation t06 / hard rule #7). Add ImageEditJsonSchema.safeParse so a
malformed body (non-object / wrong types) is rejected with 400 instead of
silently parsed; valid JSON/data-URL bodies behave exactly as before. (#3214, #3215)
- fix(dashboard): remove a duplicate handleToggleProxyEnabled /
handleTogglePerKeyProxyEnabled / handleDistributeProxies block in
providers/[id]/page.tsx — a bad merge of the proxy PRs declared all three twice,
breaking the webpack build ("Identifier already declared"). The removed copy was
byte-identical to the kept one. (#3170, #3171, #3172)
Finalize the 3.8.11 cycle CHANGELOG and clear the failures the full test:unit
gate surfaced (release-branch drift — PR merges bypassed pre-push):
- CHANGELOG: date the [3.8.11] section (2026-06-05) + repo-housekeeping roll-up
- docs(env): document THEOLDLLM_NAV_TIMEOUT_MS in .env.example + ENVIRONMENT.md
(env-doc-sync gate; #3217 added the var without docs)
- test(nvidia): exercise the #3226 bypass-fetch path via a local HTTP server and
hoist the validator import (patching globalThis.fetch no longer intercepts the
un-patched native fetch captured by proxyFetch)
- test(i18n): import the shipped normalizeComplianceEventTypes helper (#3185)
- test(model-caps): save synced metadata under the canonical gemini-3.1-pro key
now that #3229 aliases gemini-3.1-pro-high/-low to it
- test(web-session): expect the grok-web "sso + sso-rw" credential hint (#3180)
- test(synced-models): isolate DATA_DIR so the #3199 hidden-override stops
bleeding into the shared DB and breaking the re-run precondition
agy's gemini-3.1-pro-high/-low had no alias, so resolveAntigravityModelId sent the
speculative -high/-low suffix verbatim to upstream, which rejects it (400) for
gemini-3.x. Worse, the non-stream executor branch fed the 4xx response into the SSE
collector, returning a synthetic empty {object:chat.completion} envelope that masked
the error. Alias both to gemini-3.1-pro, and surface real upstream errors via
buildErrorBody for non-ok non-stream responses. + unit tests.
The Codex CLI WS->HTTP fallback rewrite (resolveResponsesApiModel) prefixes a
bare model id with codex/ whenever codex/<id> resolves to codex. Codex accepts
arbitrary model strings, so a combo name with no slash (e.g. n8n-text,
paid-premium) was rewritten to codex/<combo> and sent to Codex instead of being
resolved as a combo — regressing combos via /v1/responses in v3.8.9+. Skip the
rewrite when the bare id is a combo (getComboByName). + unit test.