Compare commits

...

204 Commits

Author SHA1 Message Date
diegosouzapw
189c867c51 Merge remote-tracking branch 'origin/release/v3.8.50' into test/9178-opencode-plugin-bare-key-suite 2026-08-08 00:43:00 -03:00
Markus Hartung
acfb844852 fix(db): resolve migration version 135 numbering collision (#9745)
Two files both claimed migration version 135:
135_connection_runtime_state.sql (#9449, landed 2026-08-07) and
135_migrate_model_capability_max_token.sql (#8908, landed 2026-08-05).
#9449 branched before #8908 merged and never got renumbered before
landing on release/v3.8.50.

This is not cosmetic: getMigrationFiles() throws "Migration version
collision detected" the moment ANY code path first touches the
database (getDbInstance() -> runMigrations()), which means a
completely fresh install/deploy from this branch cannot even boot —
confirmed live against a freshly built container while testing
unrelated live-verification tooling.

Renumbered the later-landing file to 140 (the next free slot) and
added the matching isSchemaAlreadyApplied("140") retroactive guard in
migrationRunner.ts, so a DB that already ran this migration under the
old 135 number isn't treated as needing a fresh application. This
matches the established pattern already used for the prior 135/136 ->
137/138 renumber in the same file (also caused by the same recurring
branch-before-merge numbering race).

Test plan:
- TDD: new tests/unit/migration-135-numbering-collision.test.ts (2/2)
  — spins up a hermetic fresh DB and confirms getDbInstance() applies
  every real on-disk migration without throwing, plus confirms both
  formerly-135 migrations' effects are present. Confirmed failing
  (reproducing the exact live crash) with the pre-fix colliding
  filenames restored, passing after the rename.
- npm run typecheck:core — clean
- npm run lint — clean
- npm run check:file-size — clean (migrationRunner.ts rebaselined
  1084->1094 for the new guard case)
- Full migration-runner + migration-numbering test suites (64 tests
  across 6 files) — all pass, no regressions
2026-08-08 00:42:29 -03:00
diegosouzapw
7a463bc5f9 Merge remote-tracking branch 'origin/release/v3.8.50' into test/9178-opencode-plugin-bare-key-suite 2026-08-07 23:58:26 -03:00
Diego Rodrigues de Sa e Souza
ee0f4298ca feat(plugins): expose client request headers in plugin context (#9570) (#9668)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:54:32 -03:00
Markus Hartung
0082ac5113 fix(openrouter): scope model failures per-model instead of poisoning the whole connection (#9635)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:54:29 -03:00
Arnav Rastogi
e224165689 fix(providers): mint a Zed LLM token for zed-hosted model discovery (#9628)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:54:26 -03:00
stanley
552f2e1563 fix(backend): stop reasoning replay placeholder from self-poisoning (#9573) (#9610)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:54:23 -03:00
vinogradovnet
8cb51b7922 feat(compression): add Russian language pack (#9581)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:54:20 -03:00
Maxim Seshuk
8da0aa5ba8 fix(standalone): don't await the WebDAV check before Next's listener (#9580)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:54:17 -03:00
Maxim Seshuk
7a369ba6e3 feat(audio): add Soniox STT + TTS provider (#9579)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:54:14 -03:00
backryun
e60d7f81b8 fix(types): preserve streaming PII choice keys (#9566)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:54:11 -03:00
backryun
e62516364c fix(types): validate Vision Bridge combo names (#9565)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:54:08 -03:00
backryun
c4f37ab01b fix(types): preserve sanitized tool array contracts (#9564)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:54:05 -03:00
backryun
250ea5e849 fix(types): validate Azure OpenAI base URLs (#9563)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:54:03 -03:00
backryun
8b4240831b fix(types): narrow chatCore local contracts (#9562)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:54:00 -03:00
backryun
4da8ef47c3 fix(types): align stream failure callback contracts (#9561)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:53:57 -03:00
Shixi Li
0a1f84f83f fix(classify): honor upstream retry windows on Gemini free-tier 429s (#9513)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:53:54 -03:00
nguyenha935
20a4ab6f55 fix(api): stop rejecting long chat histories by default (#9494)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:53:51 -03:00
Bob.Hou
765fd71aea feat(lib): make MODELS_DEV_SYNC_ENABLED actually control the sync (#9483)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:53:47 -03:00
Bob.Hou
29d97ac328 fix(sse): take the Antigravity output ceiling from the model, not a constant (#9482)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:53:44 -03:00
dependabot[bot]
1f77f75b69 deps: bump undici from 8.9.0 to 8.10.0 (#9472)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:53:41 -03:00
Poid-ZA
d22839626b chore(db): raise sqlite cache_size/mmap_size defaults (#9467)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:53:38 -03:00
dependabot[bot]
cd97ce5c2c chore(deps): bump docker/login-action from 4.5.2 to 4.6.0 (#9462)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:53:35 -03:00
dependabot[bot]
656526bc8d chore(deps): bump github/codeql-action/init from 4.37.3 to 4.37.4 (#9461)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:53:33 -03:00
dependabot[bot]
9f9ff3911a chore(deps): bump github/codeql-action/analyze from 4.37.3 to 4.37.4 (#9459)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:53:30 -03:00
dependabot[bot]
7b7b22cec4 chore(deps): bump github/codeql-action from 4.37.3 to 4.37.4 (#9458)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:53:27 -03:00
小妍儿 ✨
6aa9d90067 fix(ui): preserve request log position (#9452)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:53:24 -03:00
Bob.Hou
217ac4c829 feat(warmup): proactive Claude warmup scheduler (#8848) (#9449)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:53:21 -03:00
Diego Rodrigues de Sa e Souza
2f5df569b5 feat(api): add plugins marketplace install API (#6752) (#9445)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:53:18 -03:00
Diego Rodrigues de Sa e Souza
348102a114 feat(api): add response content encoding verification (#6736) (#9444)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:53:15 -03:00
Wahyu Hidayatulloh Pamungkas
e6bf92ad65 fix(usage): stop double-counting cache-read tokens in Command Code executor (#9438)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:53:12 -03:00
Pixma
99be4474b6 fix(providers): correct Codex GPT-5.6 context limits (#9432)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:53:08 -03:00
dependabot[bot]
6287caa5b3 deps: bump the npm_and_yarn group across 1 directory with 2 updates (#9427)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:53:05 -03:00
Diego Rodrigues de Sa e Souza
babc75f058 feat(chatgpt-web): harden prompt-emulated tool contract for thinking models (#7679) (#9422)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:53:02 -03:00
Chewji
062d48b1ad fix(combo): keep operator-defined model order for deterministic strategies (#9420)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:53:00 -03:00
Diego Rodrigues de Sa e Souza
74cf860ccc feat(providers): make video_url passthrough configurable per provider/model (#9248) (#9419)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:52:57 -03:00
Diego Rodrigues de Sa e Souza
915b383a71 fix(qoder): surface qodercli stderr in error message instead of generic 502 (#9319) (#9412)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:52:54 -03:00
Bob.Hou
7857e0ac6d refactor(sse): move the thinking-budget helpers out of base.ts (#9381)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:52:51 -03:00
Bob.Hou
f22b81c2d2 fix(sse): drop the localDb barrel imports from chat and auth (#9380)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:52:48 -03:00
Bob.Hou
c32818e738 test(quota): wait for the hot-path consumption instead of sleeping (#9365)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:52:45 -03:00
Milan Soni
696435efa8 fix(routing): normalize time remaining in reset-window strategy (#9330) (#9353)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:52:42 -03:00
Bob.Hou
a57e4ab873 fix(auth): let an agy request find the connection it authorized (#9340)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:52:38 -03:00
Diego Rodrigues de Sa e Souza
9dd6251361 feat(dashboard): render provider API key registration links (#9270) (#9336)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:52:35 -03:00
Diego Rodrigues de Sa e Souza
e3769ba709 feat(providers): accept JSON cookie objects in normalizeSessionCookieHeader (#9284) (#9335)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:52:32 -03:00
Diego Rodrigues de Sa e Souza
7fb3b7558a feat(providers): support max reasoning effort for opencode-zen DeepSeek models (#9318) (#9334)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:52:29 -03:00
SAMUEL AUGUSTO GUIMARAES LOPES
236aad07c7 fix(mcp): stop DB init logging from corrupting the stdio JSON-RPC stream (#9281)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:52:26 -03:00
SAMUEL AUGUSTO GUIMARAES LOPES
41d2e4e7a1 test(compression): lock in stacked RTK+Caveman savings on redundant tool_result content (#9278)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:52:23 -03:00
小妍儿 ✨
12e5c83692 fix(chat): resolve stored combo names before image-model validation (#8986) (#9027)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:52:20 -03:00
小妍儿 ✨
305a9d5f37 docs(troubleshooting): document the chat_admission_busy 503 and how to tune heavyweight chat concurrency (#9021)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:52:17 -03:00
小妍儿 ✨
9dc8cdbcb1 fix(kiro): keep interleaved tool results grouped without dropping assistant text (#8903) (#8931)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:52:13 -03:00
小妍儿 ✨
c73af2761e fix(providers): expose dual-auth actions for CodeBuddy CN (#8921)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:52:10 -03:00
小妍儿 ✨
3f4f2000b6 fix(proxy): isolate registry credentials from autofill (#8883)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:52:06 -03:00
小妍儿 ✨
124f64a6c0 fix(cli): default Codex wire API to responses (#8876)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:52:03 -03:00
小妍儿 ✨
5d71f47815 fix(opencode): complete generated model limits (#8869)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:51:59 -03:00
Diego Rodrigues de Sa e Souza
6c22f8d4c3 fix(api): specialty model catalog ignores hidden OpenRouter model flags (#9293)
The specialty model catalog loops (image, rerank, audio, moderation, video,
music) in catalog.ts reduced OpenRouter model IDs to only the final path
segment via .split("/").pop() before calling getModelIsHidden(), so stored
hidden flags with full provider-relative paths (e.g. openrouter+google/chirp-3)
were never matched.

Fix: introduce a shared getSpecialtyModelRelativeId helper that strips only
the provider prefix (like the embedding loop already did), and apply it to
all 6 affected specialty loops. Also add a hidden-model guard to the live
OpenRouter catalog path that had no such check at all.

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-07 18:09:17 -03:00
Diego Rodrigues de Sa e Souza
a651ffa66a fix(backend): use accumulated responseBody for provider payload to avoid stale dashboard log viewer data (#9315)
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-07 18:09:12 -03:00
Diego Rodrigues de Sa e Souza
df64220087 fix(web-search): bind each search provider attempt to its connection proxy (#9201)
* fix(web-search): bind each search provider attempt to its connection proxy (#9201)

The search path resolved credentials but never resolved the connection
proxy, so the upstream fetch always egressed directly. The connection-test
path already used the proxy correctly, proving the gap was in the
data-plane transport binding.

- Resolve the connection proxy before each upstream attempt using the
  existing resolveProxyForConnection(connectionId, apiKeyId, providerId)
  precedence chain, then wrap the fetch in runWithProxyContext so the
  patched globalThis.fetch routes through the configured proxy.
- Resolve and bind the alternate connection proxy independently during
  failover, so the primary account's context never leaks into the fallback.
- Carry connectionId and apiKeyId through SearchHandlerOptions into the
  route and executeWebSearch callers.
- Add connectionId to all saveCallLog entries in tryProvider, so the
  regular call log identifies the account.
- Emit a sanitized logProxyEvent per real upstream search attempt with
  provider, connection ID, proxy level, status, duration, and target
  origin/path (no query, API key, or proxy credentials).
- Cover both POST /v1/search and executeWebSearch() consumers (MCP,
  internal, skills) since both bypassed the same proxy binding.

* fix(sse): extract search proxy binding into leaf module to fit file-size cap

Move the per-attempt proxy resolution, proxied fetch, sanitized proxy-event
emission, and response handling for web search providers out of
open-sse/handlers/search.ts into a new open-sse/handlers/search/searchProxy.ts,
so the provider-dispatch chokepoint (tryProvider) stays a thin wiring call and
search.ts fits back under the frozen file-size cap (1536 lines).

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-07 18:09:08 -03:00
Diego Rodrigues de Sa e Souza
09e4c150c1 fix(credential-health): scheduler never retries failed connections due to static interval comparison (#9289)
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-07 18:09:00 -03:00
Diego Rodrigues de Sa e Souza
6d99a01a0b fix(catalog): cache getModelsDevPricing() to prevent OOM at startup (#9300)
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-07 18:08:56 -03:00
Diego Rodrigues de Sa e Souza
d92e984fec fix(providers): bump qwen-web SPA version header from 0.2.66 to 0.2.81 (#9304)
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-07 18:08:52 -03:00
Diego Rodrigues de Sa e Souza
85e518b7f4 fix(qoder): include actionable CLI_QODER_BIN hint in connection test when qodercli is not found (#9277)
When the Qoder CLI (qodercli) is not detected by getCliRuntimeStatus after
an OmniRoute restart (e.g. restricted launch context on Windows where
APPDATA/PATH are not inherited), the connection test showed only the
non-actionable 'Local CLI runtime is not installed'. Now it surfaces the
same buildQoderCliNotFoundHint guidance already used in the executor path,
telling the user to set CLI_QODER_BIN to the absolute path of qodercli.

Closes #9277

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-07 18:08:48 -03:00
Diego Rodrigues de Sa e Souza
3be585ef41 fix(providers): use prefix regex for web search fallback detector to catch versioned tool types (#9279)
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-07 18:08:44 -03:00
Diego Rodrigues de Sa e Souza
2e71558a0f fix(providers): modal.com validation returns clear error when Base URL is missing (#9102)
Modal (modal.com) is bring-your-own-deploy and requires a Base URL pointing to the
user's OpenAI-compatible Modal app. The connect-connection form labels the Base URL
override field as Optional, but the modal validator does not handle the empty case:
when no Base URL is set it passes normalizeBaseUrl('') into validateOpenAILikeProvider,
which builds an empty probe URL and trips parseOutboundUrl, leaking the raw guard
message 'Invalid outbound URL: '.

Fix: guard the empty/whitespace baseUrl case in the modal specialty validator and
return a clear, actionable error message explaining that a Base URL is required.
Add a regression test asserting the fix.

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-07 18:08:39 -03:00
Diego Rodrigues de Sa e Souza
771d3e363a fix: make antigravity and agy equivalent in credential selection (#9204)
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-07 18:08:35 -03:00
Diego Rodrigues de Sa e Souza
0bc72cfd65 fix(providers): manual Vision capable override does not affect Combo routing (#9195)
Three linked bugs prevented the Custom Models 'Vision capable' toggle from
affecting Combo routing, causing 400 capability_mismatch on image requests
sent through Combos targeting a custom vision model.

Bug #1 (catalog, dead guard): modelType === 'chat' was always false for
chat models because modelType was only assigned 'embedding', 'rerank',
'image', or 'audio'. Changed the guard to !modelType || modelType ===
'chat' so getCustomVisionCapabilityFields() fires for custom chat models.

Bug #2 (catalog, synced-first ordering): When a model appeared in both
syncedAvailableModels (from discovery) and customModels, the custom row
was skipped entirely, losing the vision override. Now merge vision fields
into the existing synced entry when the custom model has an explicit
supportsVision boolean.

Bug #3 (routing capabilities): getResolvedModelCapabilities() /
resolveVisionCapability() had no path to consult the customModels
supportsVision flag. Added a sync DB lookup helper and a new
customVisionOverride parameter so the dashboard toggle affects Combo
routing.

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-07 18:08:30 -03:00
Diego Rodrigues de Sa e Souza
46e5dfdc8f fix(lmarena): emit Uint8Array SSE chunks instead of strings to satisfy shared pipeline contract (#9237)
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-07 18:08:26 -03:00
Diego Rodrigues de Sa e Souza
41d16c9bb4 fix(api/analytics): stop charging :free models at arbitrary fallback price (#9054)
resolveModelPricing() in analytics route fell back to
Object.keys(providerPricing)[0] when a model had no pricing
entry. For OpenRouter, the defaults layer always contributes
an 'auto' record as the first key, so every :free model was
charged at that arbitrary rate in the analytics dashboard.

Fix: short-circuit :free models to return null before the
last-resort fallback, and remove the Object.keys(...)[0]
arbitrary-substitution fallback.

Closes #9054

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-07 18:08:22 -03:00
Diego Rodrigues de Sa e Souza
cfeea5dc5b fix(cli): enable systray2 on Windows for Norton-friendly tray (#8609)
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-07 18:08:17 -03:00
Diego Rodrigues de Sa e Souza
6aac7b0c8f fix(opencode): propagate vision capability from live catalog into opencode.json (#8960)
The opencode config generator fetched the live /v1/models catalog but only
extracted context_length for new model entries, discarding capabilities
(capabilities.vision, input_modalities, etc.) that OpenCode uses to gate
clipboard/image input. Newly discovered vision-capable models were presented
as text-only, causing OpenCode to reject attachments before sending the HTTP
request.

- Add input_modalities/output_modalities to CatalogModelEntry
- Add deriveOpenCodeCapabilities() helper mapping catalog capabilities to
  OpenCode fields (attachment, reasoning, temperature, tool_call) with
  explicit user override precedence
- Replace the existing round-trip-only flag loop in buildModelEntry() with
  the new helper so catalog-derived values fill in for new models

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-07 18:08:13 -03:00
Diego Rodrigues de Sa e Souza
bf8277ad46 fix(ui): normalize Free Pool API response payload to read from data.proxies (#9046)
The GET /api/settings/free-proxies route returns { success, data: { proxies, total, ... } }
since #6909, but FreePoolTab.loadData() was reading data.items and data.total from the
top-level JSON — both undefined, causing the proxy table to always show as empty
despite synced stats rendering correctly from the separate /stats endpoint.

Fix: normalize the payload with body?.data ?? body fallback so both the current
nested contract (data.proxies) and any legacy top-level shape work.

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-07 18:08:08 -03:00
Diego Rodrigues de Sa e Souza
c50c783549 fix(proxies): resolveProxyForConnection now returns the proxy name, so the dashboard badge shows the name instead of the hostname (#8995)
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-07 18:08:04 -03:00
Diego Rodrigues de Sa e Souza
ebddc51575 fix(providers): gate premium opencode-zen/opencode-go models behind an API key (#8681)
* fix(providers): gate premium opencode-zen/opencode-go models behind an API key (#8681)

Root cause: the free/noauth opencode provider (and opencode-zen/opencode-go)
expose the full upstream model list including PREMIUM models (gpt-5, claude-*,
gemini-*, kimi-k2.6, etc.). With a keyless connection, the executor sends no
Authorization header and upstream returns 401 'Missing API key' for any
premium model — which is the exact string the client shows.

Fix: add a request-time gate in OpencodeExecutor.execute() that detects
keyless connections + premium models and returns a clear 402 error with
message 'This model requires an opencode API key — add one in Settings →
Providers.' instead of proxying the raw upstream 401.

Free models (known free catalog +  suffix) continue to work keyless
(deepseek-v4-flash-free, big-pickle, etc.). Users with a valid opencode API
key keep premium access. opencode-go has no free tier — all models require
a key.

* fix(providers): use a free opencode model in the #7993 proxy-routing test

The #8681 keyless-premium gate short-circuits 'grok-code' (a premium
model) with 402 before any fetch happens, so the proxy-egress assertion
never saw a request. Swap to 'deepseek-v4-flash-free' (already applied
to the sibling opencode-proxy-rotation-4954.test.ts in this same PR)
so the test again exercises the proxy-routing path it targets.

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-07 18:07:59 -03:00
Diego Rodrigues de Sa e Souza
4299085da1 fix(db): stream DB backup export instead of buffering entire file into memory (#9045)
The GET /api/db-backups/export route used fs.readFileSync + new Response(buffer) which buffered the entire database backup into memory — for a 280MB DB this spiked RSS to ~1.5GB (5.3x the DB size), causing timeouts on constrained machines.

Fix: stream the backup file as a ReadableStream response body using fs.createReadStream + ReadableStream, keeping peak RSS under 0.5x the DB size. Includes cleanup on stream completion, error, and client abort.

Also: changed fs.copyFileSync to await fs.promises.copyFile in node:sqlite, bun, and sql.js adapters so the backup() call does not block the event loop during a large DB copy.

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-07 18:07:55 -03:00
Diego Rodrigues de Sa e Souza
48b17ff2b7 fix(build): remove misleading open-sse/package.json facade and add workspace typecheck gate (#8781)
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-07 18:07:50 -03:00
Diego Rodrigues de Sa e Souza
a76bee9f3e fix(providers): switch Antigravity quota RPCs to iterate ANTIGRAVITY_RUNTIME_BASE_URLS (#8965)
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-07 18:07:46 -03:00
Diego Rodrigues de Sa e Souza
d12c3b37da fix: resolve two macOS-only test/script failures in unit suite (#8577)
bin/restore-policies.sh used readarray (bash 4+), which fails on macOS
bash 3.2. Replace with a compatible while-read loop.

machineId.test.ts disableWindowsRegistryStrategy() did not neutralize
the macOS ioreg strategy, so mocked os.hostname() was never reached
on macOS and both ladder tests failed. Stub execSync for ioreg commands
so the fallback chain reaches os.hostname() as intended.

Production src/shared/utils/machineId.ts is correct and unchanged.

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-07 18:07:42 -03:00
Diego Rodrigues de Sa e Souza
1b83b337b3 fix(cli): fall back to node:sqlite when better-sqlite3 constructor throws at runtime (#8826)
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-07 18:07:38 -03:00
Diego Rodrigues de Sa e Souza
7be4e55e7e fix(opencode-zen): add current free-tier models to registry to enable combo context pre-filtering (#8841)
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-07 18:07:34 -03:00
Diego Rodrigues de Sa e Souza
904e8af09a fix(opencode): prefix provider id with opencode- for auth login command (#8830)
The bundled @omniroute/opencode-plugin registers its provider under
'opencode-omniroute' (the 'opencode-' prefix is required by OpenCode
>=1.17.8's native-adapter gate on model providerID). But the CLI
instructed 'opencode auth login --provider omniroute' — the unprefixed
id — so OpenCode reported 'Unknown provider "omniroute"' because it
resolves --provider against the exact provider id the plugin registered.

Add resolveOpenCodeAuthProviderId() helper that idempotently adds the
'opencode-' prefix when absent, and use it everywhere the CLI builds
or prints the --provider argument: resolveOpenCodeAuthSpawn args,
runOpenCodeAuth ENOENT message, and runSetupOpenCodeCommand 'Run
manually'/'Next step' messages. Update the plugin README and test
assertions to match.

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-07 18:07:30 -03:00
Diego Rodrigues de Sa e Souza
7fce2d55a4 fix(combo): replace tab-indented blocks with spaces
Fix TS1128 parsing error. Tabs introduced by #9630 in combo.ts replaced with spaces.

Refs #9679
2026-08-07 16:58:01 -03:00
diegosouzapw
6f875f8acf fix(combo): replace tab-indented blocks with spaces in #9630 changes
The commit for #9630 introduced tab characters instead of 2-space
indentation in two blocks (handleComboChat and handleRoundRobinCombo).
Tabs in TypeScript cause TS1128 parsing errors because the parser
expects consistent space-based indentation.

Fix: replace all leading tabs with the proper 2-space indentation
level matching the surrounding codebase convention.

This restores typecheck:core to a clean state on the release branch.
2026-08-07 16:42:03 -03:00
Diego Rodrigues de Sa e Souza
02534f4e8e feat(radar): contributor + supporter claim buttons on the activation screen (#9710)
* feat(radar): add F4/T7 contributor-claim / supporter-plans link config

Pure, DB-free src/lib/radar/links.ts resolves the two outbound "get a
supporter key" URLs (contributor GitHub-OAuth claim + supporter plans
page), same env-override pattern as RADAR_FEED_URL. No pricing/value is
ever resolved here (D14) — only the link.

* feat(radar): relay F4/T7 claim/plans links via GET /api/radar/settings

Smallest-surface option per spec: no dedicated route. The existing
settings snapshot now also returns contributorClaimUrl/supporterPlansUrl
so the dashboard client never reads process.env itself. Both are plain
public URLs, gated by the same flag/auth checks as the rest of the
response.

* feat(radar): add contributor/supporter claim buttons to activation screen

F4/T7 — "I'm a contributor" opens the GitHub OAuth claim flow;
"Support the project" opens the plans/payment page. Both links come
from the settings fetch (never a hardcoded URL in this client
component) and open in a new tab. No price/value anywhere in the
copy — the destination page is the only place pricing lives (D14).

i18n: 5 new radarPage keys (claimSectionTitle, contributorButton,
contributorHint, supporterButton, supporterHint) added to all 43
locale files with the English copy as fallback value.

* docs(radar): document F4/T7 supporter-key acquisition paths

RADAR.md: new "Getting a supporter key" section covering both claim
flows, the two env-var overrides, and the current gap (no dedicated
key-paste input in the dashboard yet — POST /api/radar/settings is the
only way to set one today). ENVIRONMENT.md + .env.example: register
RADAR_CONTRIBUTOR_CLAIM_URL / RADAR_SUPPORTER_PLANS_URL for
check:env-doc-sync.

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-07 16:41:00 -03:00
Diego Rodrigues de Sa e Souza
976d670ff3 fix(combo): distinguish pre-dispatch skips from genuine failures to prevent false 503 ALL_ACCOUNTS_INACTIVE (#9630)
Closes #9630
2026-08-07 13:45:58 -03:00
Diego Rodrigues de Sa e Souza
ff679ab86e fix(sse): move Antigravity client system content to first user message to avoid upstream 429 (#9030)
Closes #9030
2026-08-07 11:23:33 -03:00
Diego Rodrigues de Sa e Souza
ebf151e057 fix(cursor): preserve tool context across multi-turn conversations when client lacks conversation_id (#9029)
Closes #9029
2026-08-07 11:23:27 -03:00
Diego Rodrigues de Sa e Souza
7d3dc0bc35 fix(vertex): route Claude models to native rawPredict and respect targetFormat overrides (#8994)
Closes #8994
2026-08-07 11:23:19 -03:00
Diego Rodrigues de Sa e Souza
5e2429ce15 fix(errorClassifier): classify ChatGPT Web SENTINEL_BLOCKED 403 as FORBIDDEN, enabling combo fallback (#8813)
Closes #8813
2026-08-07 11:23:12 -03:00
Diego Rodrigues de Sa e Souza
6629a9b698 fix(yuanbao-web): accept content field in SSE text events (upstream format change) (#8739)
Closes #8739
2026-08-07 11:23:07 -03:00
diegosouzapw
24635f7d48 Merge origin/release/v3.8.50 (post-#9688) — base wins on babysit/sweep duplicated fixes; baseline reconcile belongs to #9616 2026-08-07 11:17:03 -03:00
Diego Rodrigues de Sa e Souza
c40d4b17ee fix(ci): clear the NEW base-reds from the 08-06 merge batch (migration collision #2 + broken import) (#9688)
* test(base): realign six suites with contracts that #9100/#8990/#9009 deliberately changed

Continuing the base-red drain — every one of these reproduces on the pure tip.

- tests/snapshots/provider/translate-path.json: regenerated via UPDATE_GOLDEN=1.
  The diff is ADDITION-ONLY — the unorouter block from #9009; no existing
  provider entry changed. 3/3.
- tests/unit/provider-models-route.test.ts: ff012ff420 added onboardUser as a
  bootstrap fallback next to loadCodeAssist; the mock now excludes it from the
  discovery-URL ledger like it already excluded loadCodeAssist, otherwise it
  consumed the injected 503 and the retry assertion misfired. 59/59.
- tests/unit/responses-commentary-passthrough-6199.test.ts: #8990 (c996dc93c2)
  deliberately preserves `tools` on the TERMINAL response.completed snapshot
  (Codex CLI rebuilds its tool list from it); the assertion now pins the echoed
  tools instead of their absence. Still stripped on created/in_progress. 7/7.
- tests/unit/vision-compression-authoritative-capability-7237.test.ts:
  68cb678780 added the 'gpt-5' fragment, so the heuristic-vs-spec DRIFT this
  suite documented no longer exists; the cases now guard the agreement, keep a
  conservative-for-unknown-ids probe, and reproduce the strip-bug shape with an
  explicit false instead of deriving it. 4/4.
- tests/unit/provider-limits-proxy-fail-closed.test.ts +
  tests/unit/image-generation-route.test.ts: #9100 made the proxy reachability
  probe NON-BLOCKING (optimistic dispatch; the probe aborts only in-flight
  requests — its own t14 sibling was updated to this exact pattern). Instant
  mocks therefore won the race and the PROXY_UNREACHABLE 503 became unobservable
  (a success or a generic 502). The mocks now stay in flight (never-resolving,
  so the aborted continuation cannot reach the restored real fetch), and the
  fail-closed proof is the settled rejection itself plus zero egress AFTER the
  fast-fail. Production fail-closed semantics are unchanged — the proxy dispatch
  path still throws; only the mock timing was stale. 3/3 and 20/20.

Refs #9298

* fix(guardrails): forward the router deps seam through callVisionModel

tests/unit/guardrails/vision-bridge-sse-and-reasoning.test.ts was 7/7 red on any
clean box (CI shard 3/4): callVisionModel() called getBestVisionModel()/
getFallbackModels() WITHOUT the routers' existing VisionBridgeRouterDeps seam,
so the credential check always hit the live connections DB — no vision-capable
connection meant 'No vision-capable provider connected' before the mocked fetch
was ever reached, and on a dev box auto-selection could swap the fixed model
under the assertions.

The routers already accepted deps; only the forwarding was missing. Added the
optional 5th param (backward compatible — the sole production caller,
visionBridge.ts, injects its own callVisionModel and is unaffected) and the
suite now pins selection with hasUsableCredentials: async () => null
(indeterminate → the fixed model is honored, DB untouched). 7/7.

Sibling suites re-run green: vision-bridge-callmodel 2/2, visionBridge 25/25,
visionBridgeHelpers.callVisionModel 8/8, visionBridgeRouter 10/10,
vision-bridge-cc-no-reroute 8/8.

Refs #9298

* fix(db,combo): clear the NEW base-reds the 08-06 merge batch introduced

The tip moved while the first sweep PR (#9600) was in review, and three fresh
base-reds landed with it — same classes as before, all reproduced on the pure
tip 9995bc4893:

1. ANOTHER migration collision: #9061 shipped 134_ccr_blocks.sql onto the slot
   134_proxy_logs_egress_ip.sql (#9291) has held since 08-04. getMigrationFiles()
   throws on collision, so every DB-touching test died at bootstrap again.
   Renumbered to 139 (next free slot). No retroactive guard needed this time:
   both statements are IF NOT EXISTS, and no DB can have applied it as 134 —
   the runner refused to run at all while the collision existed.
2. BROKEN IMPORT killing the combo module graph: #8894 imported
   preferAntigravityConnectionsWithStoredProject from
   ../antigravityProjectPersistence.ts — a module that exists NOWHERE in the
   repo (it came from an unmerged sibling branch). Anything importing
   quotaStrategies.ts died with ERR_MODULE_NOT_FOUND. Implemented the helper in
   the real persistence module (antigravityProjectPersist.ts, #8491) with the
   semantics the call site needs — prefer connections that already carry a
   stored projectId, never emptying the pool — and pointed the import there.
   New regression suite tests/unit/antigravity-prefer-stored-project.test.ts
   (5/5), including an import-graph probe that reproduces the break shape.
3. Sibling-test drift from #9106 (gemini-3.1-pro-high now user-callable): its
   own suites were updated but provider-models-route.test.ts was not. Expected
   discovery list realigned; testFrozen 1784->1787 justified in the baseline
   (irreducible +2 after comment compression; gate counts split-newlines).

Also regenerated tests/snapshots/provider/translate-path.json — addition-only:
devin-cli-agentic, raycast, regolo (today's provider merges), zero removals.

image-generation-route 20/20 (was import-dead), provider-models-route 59/59,
antigravity-prefer-stored-project 5/5, provider-translate-path-golden 3/3.

Refs #9298

* fix(changelog): convert the #9415 fragment to the required bullet shape

Another base-red from the 08-06 batch: bd4407cb64 landed
changelog.d/features/9415-newapi-sub2api-aggregator-balance.md as YAML
frontmatter + a prose paragraph. Every other fragment in changelog.d/ is a
single markdown bullet, and both consumers enforce that —
scripts/check/check-changelog-integrity.mjs:97 and the release aggregator
(scripts/release/aggregate-changelog.mjs:57) reject anything that does not
start with '- ', so 'Merge integrity (changelog + generated skills)' was red
for every PR targeting the release branch.

Rewritten as a bullet with the standard issue link, preserving the feature
description (aggregator gateway toggle, /api/user/self balance read, dashboard
badge, quota-preflight skip, NEWAPI_AGGREGATOR_BALANCE flag default off,
quotaPerUnit override). Swept the rest of changelog.d/ — this was the only
malformed fragment.

check:changelog-integrity OK.

Refs #9298

* fix(types,docs): clear the 5 typecheck errors and the fabricated env vars on the base

Third pass over the base-reds, from the 2026-08-06T22:51Z verdict on #9298 —
it reported "Typecheck (core)" with only the FIRST error; there are five, all on
the pure tip 9995bc4893. Two are real production defects.

**Real bugs**

- open-sse/services/compression/engines/ccr/index.ts:295 called
  enforceGlobalBudget(entry.bytes) against an (owner, bytes) signature. The
  `bytes` argument arrived undefined, so `ccrTotalBytes + undefined` is NaN,
  `NaN > MAX` is false (the eviction loop exits immediately) and `NaN <= MAX` is
  false (the re-admit is refused). The #9061 durable tier therefore NEVER
  repopulated its in-memory map: every retrieve after a restart or an eviction
  re-read from SQLite forever, and evictions could not prefer the owning
  principal. Fixed and pinned by a new case in
  tests/unit/ccr-durable-store-9061.test.ts (11/11) — verified failing against
  the buggy call and passing against the fix.
- open-sse/services/combo/fusionPanel.ts:54 read `step.model` after #8894
  widened ComboStep with ComboProviderWildcardStep (which carries modelPattern,
  not model), so a wildcard step in a fusion panel pushed `undefined` onto the
  panel. Now resolved through getComboModelString(), which already handles every
  step shape and returns null for the ones without a concrete model id.

**Type-only**

- accountSemaphore.ts:203 — isBypassed() returns a plain boolean and cannot
  narrow `number | null` (an `x is null | undefined` predicate would be unsound:
  0 bypasses too). Added resolveActiveCap(), the narrowing companion isBypassed
  is now defined in terms of; the acquire path uses the narrowed value.
- comboStructure.ts:140 — same #8894 widening: `prompt` only exists on a model
  step, so it is now read under a kind check.
- firecrawlQuotaFetcher.ts:136 — the function returns full FirecrawlQuota
  objects but was annotated Promise<QuotaInfo | null>, which made the
  custom-base literal an excess-property error. Widened to the accurate type
  (FirecrawlQuota extends QuotaInfo, so callers are unaffected).

**Fabricated docs (the "Docs sync + fabricated-docs (strict)" HARD failure)**

docs/ops/VM_DEPLOYMENT_GUIDE.md recommended OMNIROUTE_MAX_POOL_SIZE and
OMNIROUTE_DB_POOL_SIZE (#9471). Neither is read anywhere in the codebase.
Replaced with the two knobs that do exist and are already documented in
ENVIRONMENT.md: OMNIROUTE_MEMORY_MB and OMNIROUTE_CHAT_MAX_HEAVY_IN_FLIGHT.

typecheck:core 5 errors -> 0. check:fabricated-docs + check:env-doc-sync OK.
accountSemaphore 6/6, ccr-durable-store 11/11, ccr-protocol 9/9,
combo-fusion-strategy 10/10, combo-fusion-comboref 5/5, combo-fusion-warn 4/4,
firecrawl-executor 7/7, executor-firecrawl-fetch 4/4.

Refs #9298

* fix(tests): type the #3440 vertex helpers instead of `any` (the 3 base ESLint errors)

The "ESLint errors: 3 error(s)" HARD failure in the #9298 verdict is
tests/unit/vertex-functioncall-id-3440.test.ts lines 32/41/50: the three
find*(result: any) walkers. `@typescript-eslint/no-explicit-any` is an ERROR in
tests/ (and open-sse/) since #6218, and this file landed on 2026-08-04 without a
suppressions entry, so every run of `lint:json --max-warnings 0` failed. That
step prints nothing on failure, which is why the gate looked like a silent
crash across the open PRs.

Replaced with a GeminiRequestLike interface describing exactly what the three
walkers traverse (contents[].parts[]), so the assertions keep their meaning and
nothing is cast away.

eslint on the file: clean. Suite: 6/6.

Refs #9298

* docs(proxy): use an RFC 5737 documentation IP in the proxy examples

The #9298 verdict headlines its docs failure with
`L810 [stale-version] 1.2.3: const removed = await failOneproxyProxy("1.2.3.4", 8080)`.
That is a false positive: check-deprecated-versions.mjs matches
`/\bv?[12]\.\d+\.\d+\b/`, and the example IP literal 1.2.3.4 contains "1.2.3".

Swapped both occurrences in PROXY_GUIDE.md (and its pl mirror) for 203.0.113.7,
from the RFC 5737 documentation range that exists precisely for examples — it
cannot collide with a version pattern and is the correct thing to print in docs
regardless. Drift count 64 -> 62; no gate threshold was touched.

The gate that actually FAILED under "Docs sync + fabricated-docs (strict)" was
check:fabricated-docs (the invented pool env vars), fixed in the previous
commit; this one removes the misleading line the verdict quotes.

* test(base): allowlist probeUtils and realign the #7849 suite to the replacement bound

Two more base-reds, both visible only after the migration collision stopped
killing the shards.

**check-db-rules — src/lib/db/probeUtils.ts not classified**

#9541 added probeUtils.ts (transient-error retry for the SQLite corruption
probe). It is imported ONLY by src/lib/db/core.ts, exactly like its siblings
schemaColumns / optimizationSettings / providerNodeSelect, so re-exporting it
through localDb.ts would push callers toward the barrel-import anti-pattern the
gate exists to prevent. Added to INTENTIONALLY_INTERNAL with that rationale.
check-db-rules 22/22, check:db-rules exit 0.

**session-dedup-memory-7849 — pinned a mechanism that was replaced**

7f36b192f0 (#7855 follow-up) swapped the shared "suffix work budget" for the
MAX_SUFFIX_STARTS / MAX_TOTAL_BLOCK_BYTES guards and deleted both the budget and
its SUFFIX_WORK_BUDGET_WARNING string. It updated session-dedup.test.ts but not
this sibling, so 3 of its 4 cases asserted a warning that can no longer be
emitted.

Realigned to the contract that actually survives — which is the invariant #7849
was opened for, not the mechanism:
- the pathological pair must stay BOUNDED (completes in <4s, body intact) —
  measured at ~280ms on the current guards;
- it must FAIL OPEN — original body returned by identity, compressed false,
  stats null (the explanatory zero-savings stats belonged to the removed
  budget path, which skipped before producing any);
- the 512 MiB child fixture must still exit 0 with the full engine chain
  (session-dedup, lite, rtk, headroom, caveman) — that IS the OOM guard — and
  session-dedup must still report its skip, now pinned by prefix since the
  reason string moved with the mechanism.

No threshold was loosened and no case was deleted: 4/4 here, 8/8 on the sibling
session-dedup.test.ts.

Refs #9298

* docs(mcp): bump the tool count to 105 and realign two vitest count pins

Three more base-reds from the same 08-06 batch, all count/contract drift that
the merged PRs left in sibling files.

**Docs Gates (fast-path) — 3 STRICT drifts**

check:docs-counts measures the MCP tool set from live code: it is 105 now
(#8925 added omniroute_create_combo), while README.md, AGENTS.md and
docs/frameworks/MCP-SERVER.md still claimed 104. Updated all five occurrences
(two of them inside SVG alt text). check:docs-all exits 0.

**Vitest (fast-path) — 2 failures**

- open-sse/mcp-server/__tests__/essentialTools.test.ts pinned 11 phase-1 tools;
  #8925 shipped omniroute_create_combo as phase 1, making it 12. Verified by
  enumerating MCP_ESSENTIAL_TOOLS directly.
- tests/unit/autoCombo/provider-family-combos.test.ts pinned the auto/glm
  provider set to [auggie, glm, zai]. #8914 (Devin ACP bridge) added
  devin-cli-agentic, whose catalog (registry/devin/catalog.ts:90-93) advertises
  the glm-5-2* line — so it belongs in the family pool for exactly the reason
  the test's own comment gives for auggie: a no-auth backend that genuinely
  serves a family model is a legitimate member. Expected set updated, invariant
  unchanged.

npm run test:vitest 36/36 files, 340/340 tests.

Refs #9298

* fix(combo,usage,oauth): drain the base-reds the shard fix exposed

With the migration collision and the broken import out of the way the four unit
shards actually run, and a further layer of base-reds became visible on the pure
tip 9995bc4893. Three are production defects.

**Production defects**

- open-sse/services/combo/runtimeUnitCapacity.ts:58 called resolveComboTargets()
  WITHOUT the hidden-model snapshot, so it fell back to the default
  getHiddenModelsByProvider() — a fresh full key_value read PER nested combo-ref
  unit, on every request. #8878 threaded the snapshot through the other call
  sites and missed this one. Threaded it from executeRuntimeUnitCombo (and from
  the dispatchPrelude call site), restoring the one-snapshot-per-request
  invariant combo-hidden-leaf-routing.test.ts pins. 9/9.
- open-sse/services/usage/firecrawl.ts silently ignored its own `apiKey`
  parameter: 91bb6aa619 moved the fetch to
  fetchFirecrawlQuota(connectionId, connection), which reads the key off the
  connection record, so any caller passing the key directly got "Firecrawl API
  key not available". The explicit key is now merged into the connection passed
  down. firecrawl-usage 8/8.
- src/lib/oauth/constants/oauth.ts was missing a RAYCAST entry in PROVIDERS
  while src/lib/oauth/providers/index.ts registers `raycast` (#8895), so every
  consumer reading PROVIDERS did not know Raycast Pro exists. Also added its
  OAUTH_TEST_CONFIG entry (checkExpiry only — it is an `import_token` provider
  with refreshToken always null), which #8408's guard explicitly requires rather
  than grandfathering. oauth-providers-config 25/25, oauth-test-config-8408 2/2.

**Count / contract drift from the same batch**

- feature flags 45 -> 46, APIKEY_PROVIDERS 197 -> 198 (Raycast Pro #8895),
  unique MCP tools 107 -> 108. Each re-derived from the source of truth.
- vi + pt-BR locales: translated the 8 keys #9415 added
  (providers.newApiAggregator* and providers.modelTestQuotaTooltip) instead of
  relaxing the parity guard. i18n-vi 5/5, i18n-pt-br 3/3.
- login-bootstrap-route: #9491 added `authenticated` to the require-login
  payload so /login can redirect an active session; the three deepEqual bodies
  now carry it. 10/10.

**Flaky-by-construction, made deterministic**

tests/unit/chat-combo-live-test.test.ts asserted the early-keepalive frame with
a 100ms mocked upstream while resolveKeepaliveThreshold() is 2000ms for
openai/*. It only ever passed while unrelated handler latency happened to push
the total past the threshold — incidental, not deterministic, and it stopped
holding once the handler got faster. The mock now sleeps 2400ms so the slow path
is guaranteed and the assertion means what it says. 5/5.

typecheck:core exit 0. check:file-size (base-relative) OK.

Refs #9298

* test(base): run the orphaned #8890 suite and realign three mechanism pins

**check:test-discovery — a suite that had NEVER executed**

#8890 landed open-sse/services/__tests__/fail-fast-concurrency-gate.test.ts into
a directory no runner collects (only one explicit file from that folder is in
vitest.mcp.config.ts), so it ran zero times since it merged. Wired it into the
runner AND into check-test-discovery.mjs's mirrored collector list, which the
gate keeps in sync deliberately. It passes 4/4 now that it actually runs —
test:vitest goes 36 -> 37 files, 340 -> 344 tests.

**check-db-rules-classification** — 37 -> 38 audited modules, adding probeUtils
alongside the INTENTIONALLY_INTERNAL entry from the previous commit.

**ratelimit-reservoir-refresh** — #9604 (rolling RPM leases) DELETED Bottleneck's
fixed-window reservoir, so currentReservoir() is null and the poll for
`reservoir === 2` could never settle. It updated several sibling suites but not
this one. The pin on the removed mechanism is gone; what remains is the
invariant the original Bottleneck heartbeat bug actually broke and that #9529
opened this test for — after a header-learned updateSettings() the limiter must
keep admitting work, proven by racing a post-exhaustion request against a 5s
timer. 1/1.

**translator-openai-to-gemini** — #9568 (c9a3361e5a) made
buildChangedToolNameMap emit IDENTITY entries too, because Gemini lowercases
tool names in functionCall responses and the response translator needs a key to
map them back. Any request carrying tools therefore carries `_toolNameMap` in
the Antigravity envelope now. Expected key list updated and the map's contents
asserted explicitly rather than left implicit. 45/45.

Refs #9298

* fix(db): restore node-backed synced catalogs and realign the #8944 context hints

**Production regression from #9294 (d69f521491)**

lookupModelMeta moved from getSyncedAvailableModels(providerId) to
getActiveSyncedCatalog(providerId). The new reader unions models only from rows
in `provider_connections` with isActive = 1 — but a provider NODE lives in
`provider_nodes` and NEVER has a connections row, so filtering by active
connection ids silently dropped every node's synced catalog.

The consequence was not just a missing list: lookupModelMeta reads that catalog
for RUNTIME METADATA, so for openai-compatible nodes it took out
- `supportedThinkingEfforts`, which is what splitSyncedEffortSuffix needs — so
  `<prefix>/<model>-high` stopped resolving to the base id and the effort was
  never derived (#7694), and
- `contextWindow` / `maxInputTokens`, used by the combo context-window filter.

getActiveSyncedCatalog now falls back to the provider-wide key_value set — the
exact pre-#9294 source — when no active connection carries a catalog, and marks
that fallback explicitly NON-authoritative. #9294's live-catalog gating is about
what an active connection actually serves, so a node-backed catalog informs
metadata while never being able to reject a model as unavailable. `available`
therefore stays fail-open for nodes, as it was before.

sync-reasoning-supported-efforts-7694 23/23 (was 21/2).
live-model-catalog-reconciliation-8926 11/11 and combo-provider-wildcard 23/23
confirm #9294's own coverage is untouched.

**#8944 sibling-test drift**

714a315a1a ("Treat context metadata as a routing hint") deliberately turned the
context-window check from a HARD filter into an ordering hint: a catalog-too-small
target is demoted, not removed, because a stale catalog entry must never delete
the only target that could accept the request at runtime. The PR updated one case
in this suite and left three asserting the old drop behaviour. Realigned to the
new contract — the too-small target must lose the ordering to the fitting one
while remaining present — and renamed them from "still rejects"/"still dropped"
to "is demoted"/"ordered last" so the names stop describing the removed
behaviour. 14/14.

**file-size**

tests/unit/translator-openai-to-gemini.test.ts testFrozen 1616 -> 1619: the
frozen value sat exactly at the base size, so the 3 lines the previous commit's
_toolNameMap alignment needs could not fit. Justified in the baseline.

typecheck:core exit 0.

Refs #9298

* chore(stryker): register the two covering suites missing from tap.testFiles

check:mutation-test-coverage flags any unit test that covers a mutated module but
is absent from stryker.conf.json tap.testFiles — without the entry its mutant
kills do not count toward the module's score.

- tests/unit/antigravity-prefer-stored-project.test.ts covers
  open-sse/services/combo/quotaStrategies.ts (added earlier in this PR).
- tests/unit/executor-devin-cli-agentic-acp.test.ts covers
  src/sse/services/auth.ts — pre-existing drift, same gate, same fix.

Inserted in alphabetical position only; the rest of the file is byte-identical
(it is not prettier-formatted upstream and reformatting it is out of scope here).

Refs #9298

* fix(db): drop the never-wired getSessionModelUsageCounts (knip regression)

The dead-code ratchet only ran once the earlier Fast Quality Gates steps stopped
failing, and it lands at 228 vs baseline 227.

The extra symbol is src/lib/db/contextHandoffs.ts::getSessionModelUsageCounts,
added by #8894 "for least-used strategy" and never wired: the least-used branch
in applyStrategyOrdering.ts uses the pre-existing sortTargetsByUsage(), and the
helper has no caller in src/, open-sse/ or tests/. It is the same incomplete-PR
shape as that PR's import of a module which does not exist in the repo (fixed
earlier in this branch).

Removed rather than baselined — bumping the ratchet would loosen the gate, and
removal is exactly the remedy the gate prescribes. Same treatment the Dario
installer's never-wired uninstall() got in #9600. The implementation is
recoverable from a598fbb090 whenever someone actually wires a session-aware
least-used strategy.

check:dead-code 228 -> 227 (baseline untouched). check:db-rules exit 0.
context-handoff 13/13, db-context-handoffs 7/7, service-context-handoff 11/11.

Refs #9298

* fix(security): embed the Raycast signature secret via resolvePublicCred (HR#11)

The secret-scan ratchet only ran once the earlier Fast Quality Gates steps
stopped failing, and it lands at 1 finding vs baseline 0.

The finding is open-sse/services/raycast.ts:19 —
RAYCAST_DEFAULT_SIG_SECRET, a 64-hex request-signature secret that #8895
committed as a bare string literal. It is genuinely public (community-extracted
from the Raycast macOS client; the SAME value ships to every install, it is not
a per-user credential), which is exactly the category Hard Rule #11 governs:
public upstream credentials MUST go through resolvePublicCred()
(open-sse/utils/publicCreds.ts), never a literal — see
docs/security/PUBLIC_CREDS.md.

So the fix is the mandated pattern, not a .gitleaks.toml allowlist entry: added
`raycast_sig_secret` to EMBEDDED_DEFAULTS as the XOR-masked byte sequence and
resolved it with the existing RAYCAST_SIG_SECRET env override. The
providerSpecificData.sigSecret override is untouched. Verified the decoded value
is byte-identical to the literal it replaces.

check:secrets secretFindings 1 -> 0. check:public-creds exit 0.
publicCreds 12/12, raycast-auth 6/6, raycast-local-extract 1/1,
trae-publiccred 3/3. typecheck:core exit 0.

Refs #9298

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-07 11:13:20 -03:00
Diego Rodrigues de Sa e Souza
d86ea99713 feat(radar): referral links — free-credits tab + default provider link (#9697)
* feat(radar): client-side schema + accessor for referral links (D28)

Server already publishes a signed `referrals` section on the Radar feed
({fixed, campaigns}); this adds the client mirror: RadarFeedSchema gains a
`.default()`-backed `referrals` field (old cached feeds without it stay
valid) with https-only url validation, and src/lib/radar/index.ts exposes
getRadarReferrals()/getDefaultReferralFor() (never throw: flag off, no
cache, or a corrupt/old payload all resolve to the empty shape). The
provider-default lookup itself lives in a new DB-free src/lib/radar/
referrals.ts so it stays safe to import from a "use client" component.

* feat(radar): add GET /api/radar/referrals route (D28)

Local-only route mirroring the /api/radar/catalog gate order: RADAR_ENABLED
off => 404 before any auth check (byte-identical flag-off inertia),
unauthenticated => 401, otherwise 200 with {fixed, campaigns, tier} read
straight from the local cache. Never proxies the private feed server.

* feat(dashboard): add "free credits" tab to the Radar page (D28)

Reuses the existing /dashboard/radar page instead of a new route (less
routing/i18n surface): a second tab lists fixed referral links (grouped by
provider, with requiredAction + an external-link button) and temporary
campaigns (with validUntil). When campaigns is empty and the served tier is
community, shows a soft upsell note — never gates the fixed links list,
which stays fully populated on every tier. Adds 10 new radarPage i18n keys
(English fallback) to all 43 locale files to avoid dropping i18n-ui-coverage
below threshold.

* feat(providers): use Radar default referral link on the provider name (D28)

ProviderPageHeader already linked the provider name to providerInfo.website
with a precedent for a monetized link (the Kimi partner-link note); this
lets a Radar default referral override that URL, reusing the exact same
discreet note instead of a new visual treatment.

Loose coupling: resolveProviderHeaderLink() in providerPageUtils.ts is a
pure function with no @/lib/radar or @/lib/db/* import (asserted by the new
test), so the providers dashboard never depends on the DB-touching Radar
module to render. ProviderDetailPageClient (a "use client" component) is
the only place that fetches Radar data, via the local /api/radar/referrals
route (same pattern the Radar page itself uses) and the DB-free
findDefaultReferral() helper. With RADAR_ENABLED off, no cache, or no
default referral for the provider, the header renders byte-identical to
before this feature existed.

* docs(radar): document referral links / free credits (D28)

Adds a "Referral links (free credits)" section covering the referrals feed
shape, the getRadarReferrals()/getDefaultReferralFor() accessors, the new
GET /api/radar/referrals route, the Radar page's "Free credits" tab, and
the loosely-coupled referral link on the provider-name header. Also
corrects the local-routes count (four -> five) now that /api/radar/
referrals exists.

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-07 10:17:46 -03:00
Diego Rodrigues de Sa e Souza
1e15583f29 fix(radar): close audit gaps (auth, feed fields, opt-in state, sidebar gate, size cap) + daily sync scheduler (#9686)
* fix(radar): preserve extended feed fields and honor local enable override

applyFeed()'s MergedEntry shape omitted contextWindow/capabilities/limits/
setup even though FeedModel always carries them, so the dashboard's setup
link, Context column, and capability badges never rendered and the setup
page's provider lookup always failed. Both merge paths (mergeOne and
feedModelToMerged) now copy the four fields through, respecting rule 1
(local override wins) same as every other field.

feedModelToMerged() also unconditionally forced enabled:false when the feed
disabled a feed-only entry, even when the operator had locally overridden
enabled:true — mergeOne() already applies overrides after the disable rule
and got this right. feedModelToMerged() now only force-disables when there
is no local `enabled` override, matching mergeOne()'s semantics.

* fix(radar): cap feed sync response body at 10MB

syncRadar() buffered the entire feed response via
Buffer.from(await res.arrayBuffer()) with no size limit, so a
misconfigured or hostile RADAR_FEED_URL (or an upstream serving garbage)
could force an unbounded in-memory buffer. Enforcement is two-layered: a
Content-Length preflight skips reading an already-oversized body entirely,
and a running-total check while reading the stream enforces the cap even
when Content-Length is absent or understates the real size — concatenating
the accumulated chunks preserves the exact bytes the signature check needs.

Exceeding the cap returns a new { status: "too_large" } SyncStatus and
leaves the cache untouched, following the same non-destructive pattern as
every other sync failure (invalid_signature/invalid_schema/stale).

* fix(radar): gate the sidebar radar item behind RADAR_ENABLED

The "radar" sidebar item was registered unconditionally in
sidebarVisibility/sections.ts, but Sidebar.tsx has no feature-flag
awareness (it's a client component), so the link stayed visible and
clickable with RADAR_ENABLED off, landing on a 404 dashboard page.

Sidebar items gain an opt-in `featureFlagKey` field plus a pure
isSidebarItemVisibleForFlags() filter (fails open when a flag isn't in the
map, so a missing/not-yet-loaded key never hides an unrelated item). The
resolved flag value piggy-backs on the /api/settings response the sidebar
already fetches on mount (new `radarEnabled` field) rather than adding a
dedicated round trip.

* fix(radar): require auth on management routes, add GET settings

GET /api/radar/catalog, POST /api/radar/sync, and POST /api/radar/settings
had zero authentication — any client that could reach the local server
could read the merged catalog, trigger a sync, or flip the opt-in/set the
supporter key. All three (plus the new GET below) now call
isAuthenticated() from the shared apiAuth guard, same gate as the rest of
/api/settings/*. The RADAR_ENABLED flag-off 404 check keeps running FIRST
so flag-off inertia stays byte-identical (no auth prompt just to learn the
surface doesn't exist); auth runs after it, before any DB access.

Adds GET /api/radar/settings, returning { optIn, hasSupporterKey,
supporterKeyMasked } — the raw key never leaves the server on either verb.
The dashboard page's fetchSettings() now calls this endpoint instead of
inferring opt-in state from the catalog response (which always defaulted
to unknown/null), so an already-activated operator no longer sees the
activation screen on every reload. handleSync() also handles the new
too_large sync status introduced by the response-cap fix, reusing the
existing generic sync-failed copy (no new UI strings).

* docs(radar): fix stale feed URL, document tier header/auth/size cap

- RADAR_FEED_URL default was documented as radar.omniroute.dev in
  ENVIRONMENT.md; the actual default (src/lib/radar/sync.ts) and every
  other reference use radar.omniroute.online — fix the one stale spot.
- Correct the FREE_MODEL_BUDGETS source path: it's declared in
  freeModelCatalog.data.ts, not freeModelCatalog.ts (which only
  re-exports it).
- Document that the signed feed body's `tier` is always "live" (one
  signed artifact per version) and the actually-served tier comes from
  the `x-omniroute-feed-tier` response header, resolved with a Zod parse
  + fallback to the body field.
- Document that all four /api/radar/* routes now require auth
  (isAuthenticated(), same gate as /api/settings/*), the new
  GET /api/radar/settings route, and the new too_large sync status from
  the 10MB response cap.

* feat(radar): daily sync scheduler + auto-sync on page open

Spec asks for a 1x/day sync while opted in and fresh data on every page
open. The scheduler only arms itself when RADAR_ENABLED AND the opt-in are
already on (boot) or right after the user opts in (settings route) — a
flag-off install never creates the timer, preserving the inertia contract.
The page auto-syncs once per mount when the cached feed is older than 6h.

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-07 08:26:18 -03:00
diegosouzapw
0b8c6b3a8c Revert "fix(pr): rename duplicate migration 134_proxy_logs_egress_ip to 139"
This reverts commit 1312e1a917.
2026-08-07 04:18:11 -03:00
diegosouzapw
255cc8b02a docs(changelog): fragment for #9614 2026-08-07 03:21:11 -03:00
diegosouzapw
1312e1a917 fix(pr): rename duplicate migration 134_proxy_logs_egress_ip to 139 2026-08-07 01:26:34 -03:00
diegosouzapw
9f76c63559 fix(pr): update file-size baseline for base-red drift after merging 48 base commits 2026-08-07 01:22:10 -03:00
diegosouzapw
a1a04877c9 fix(pr): remove YAML frontmatter from changelog fragment (validator expects bare bullet) 2026-08-07 01:07:56 -03:00
diegosouzapw
d1bfa7a3c0 fix(pr): fix changelog fragment format, login-bootstrap test assertions, and VM_DEPLOYMENT_GUIDE fabricated env vars 2026-08-07 00:52:53 -03:00
diegosouzapw
985e8ba44c Merge remote-tracking branch 'origin/test/9178-opencode-plugin-bare-key-suite' into babysit/pr-9614 2026-08-07 00:01:45 -03:00
Diego Rodrigues de Sa e Souza
9995bc4893 fix(security): anchor hostname comparison in Adobe Firefly login (#778)
Parse and compare hostname with dot-anchored endsWith instead of substring includes. Closes code-scanning #778.
2026-08-06 23:12:54 -03:00
Diego Rodrigues de Sa e Souza
c9a3361e5a fix(translator): add case-insensitive fallback for upstream tool call name lookups (#9575)
Closes #9575
2026-08-06 22:58:51 -03:00
Diego Rodrigues de Sa e Souza
c9debe92bd fix(translator): restore original tool name casing in Gemini response translators (#9568)
Closes #9568
2026-08-06 22:55:50 -03:00
Diego Rodrigues de Sa e Souza
fad3539a69 fix(sse): replace timer-based waits with polling to fix flaky chatCore/SSE tests under CI load (#9567)
Closes #9567
2026-08-06 22:55:44 -03:00
Diego Rodrigues de Sa e Souza
919f9acd80 fix(build): lazy-resolve module-level fs paths to avoid Turbopack NFT whole-source trace (#9560)
Closes #9560
2026-08-06 22:55:38 -03:00
Diego Rodrigues de Sa e Souza
8a573c56e3 fix(proxy): NO_PROXY now bypasses context-level proxy in resolveProxyForRequest (#9551)
Closes #9551
2026-08-06 22:55:33 -03:00
Diego Rodrigues de Sa e Souza
f338363cd3 fix(model): add aq alias for amazon-q provider so parseModel resolves it instead of falling back to OpenAI (#9550)
Closes #9550
2026-08-06 22:55:27 -03:00
Diego Rodrigues de Sa e Souza
28dc5af7ba fix(providers): strip provider prefix in getModelTargetFormat to route GPT-5.6 models to /v1/responses (#9545)
Closes #9545
2026-08-06 22:55:21 -03:00
Diego Rodrigues de Sa e Souza
616175a93e fix(search): mark searxng-search as fallbackOnly to prevent auto-select without instance (#9543)
Closes #9543
2026-08-06 22:55:16 -03:00
Diego Rodrigues de Sa e Souza
e4e0c254ea fix(db): add transient-error retry to corruption probe to prevent data loss under concurrent load (#9541)
Closes #9541
2026-08-06 22:55:10 -03:00
Diego Rodrigues de Sa e Souza
2e9abab944 fix(backend): map cache tokens in OpenAI-to-Claude non-streaming usage translation (#9536)
Closes #9536
2026-08-06 22:55:05 -03:00
Diego Rodrigues de Sa e Souza
4cc9cf8123 fix(test): prevent flaky modelsDevSync timer assertions by serializing test execution within the file (#9534)
Closes #9534
2026-08-06 22:54:59 -03:00
Diego Rodrigues de Sa e Souza
6c3aea6ba6 fix(ci): include combo-matrix tests in test-integration job (#9531)
Closes #9531
2026-08-06 22:54:53 -03:00
Diego Rodrigues de Sa e Souza
9edefd4572 fix(oauth): Kiro import token endpoint no longer overwrites existing connection when using shared cached OIDC clientId (#9435)
Closes #9435
2026-08-06 22:54:48 -03:00
Diego Rodrigues de Sa e Souza
8fbd331567 fix(compression): drop orphan custom_tool_call/local_shell_call/apply_patch_call on compaction restore (#8946)
Closes #8946
2026-08-06 22:54:42 -03:00
小妍儿 ✨
5f471181fa refactor(db): add combo repository boundary (#8757)
Validated in local merge-train (tomni-proxmox-113)
2026-08-06 19:11:57 -03:00
Lucas Mellos Carlos
ebdbe3a38f feat(mcp): add omniroute_create_combo tool (#8925)
Validated in local merge-train (tomni-proxmox-113)
2026-08-06 19:11:07 -03:00
diegosouzapw
535c75b60a fix(security): parse and compare hostname instead of substring match in Adobe Firefly login
Replace request.url.includes(FIREFLY_3P_HOST_SUFFIX) with parsed-hostname
comparison (anchored endsWith), closing CodeQL alert #778.

The old substring check could be bypassed by an attacker-controlled page
visited during the browser login window — a URL like
'https://evil.com/firefly-3p.ff.adobe.io' would pass the gate and its
Bearer token would be captured as the Adobe credential.

Practical severity is low (only during operator-initiated, time-boxed
login on a temp-profile browser), but the fix is one line and matches
the dot-anchored idiom used in docker/devin-bridge/network-guard/.

Closes code-scanning #778.
2026-08-06 19:09:49 -03:00
Adriano de Oliveira Ferreira
7f36b192f0 fix(compression): bound session-dedup suffix-block scan to prevent OOM (#8438)
Validated in post-merge-train sweep (boards clean on release/v3.8.50 tip)
2026-08-06 11:09:05 -03:00
Austin Liu
1e55fbd20b [v3.8.50] fix(open-sse): filter non-numeric values in comboTargetLimits before min calculation (#8774)
Validated in post-merge-train sweep (boards clean on release/v3.8.50 tip)
2026-08-06 11:08:59 -03:00
Andrew B.
7bb4bfc4fb fix(combo): fail-fast concurrency gate and execute-mode overflow (#8890)
Validated in post-merge-train sweep (boards clean on release/v3.8.50 tip)
2026-08-06 11:08:51 -03:00
Andrew B.
0e1f40ed1f feat(oauth): add Raycast Pro provider with local auto-import (#8895)
Validated in post-merge-train sweep (boards clean on release/v3.8.50 tip)
2026-08-06 11:08:44 -03:00
Lucas Israel
2a94cbfe14 feat(executors): add isolated Claude Code bridge over Devin ACP (#8914)
Validated in post-merge-train sweep (boards clean on release/v3.8.50 tip)
2026-08-06 11:08:37 -03:00
rinseaid
813dbb6e03 fix(vision): prevent bridge streaming and normalize OMP effort (#8945)
Validated in post-merge-train sweep (boards clean on release/v3.8.50 tip)
2026-08-06 11:08:31 -03:00
Aman
d69f521491 fix: reconcile active live model catalogs (#9294)
Validated in post-merge-train sweep (boards clean on release/v3.8.50 tip)
2026-08-06 11:08:23 -03:00
Andrew B.
a598fbb090 fix(combo): least-used quota strategy and wildcard UI preservation (#8894)
Validated in post-merge-train sweep (boards clean on release/v3.8.50 tip)
2026-08-06 11:08:02 -03:00
Jan Leon
714a315a1a Treat context metadata as a routing hint (#8944)
Validated in post-merge-train sweep (boards clean on release/v3.8.50 tip)
2026-08-06 11:07:56 -03:00
hppsc1215
274514405f fix(oauth): GHE Copilot OAuth lifecycle — connect, manual refresh, proactive refresh (#8970)
Validated in post-merge-train sweep (boards clean on release/v3.8.50 tip)
2026-08-06 11:07:49 -03:00
Alexey Gusev
91bb6aa619 feat(providers): add comprehensive support for self-hosted Firecrawl via FIRECRAWL_BASE_URL and custom base URLs (#9052)
Validated in post-merge-train sweep (boards clean on release/v3.8.50 tip)
2026-08-06 11:07:42 -03:00
Bob.Hou
404554caeb fix(antigravity): alias gemini-3.1-pro-high to gemini-pro-agent (#9106)
Validated in post-merge-train sweep (boards clean on release/v3.8.50 tip)
2026-08-06 11:07:35 -03:00
Fajar Hidayat
8e27f5ec8d fix(sse): back the CCR block store with a durable tier (#9061) (#9198)
Validated in post-merge-train sweep (boards clean on release/v3.8.50 tip)
2026-08-06 11:07:28 -03:00
diegosouzapw
56c8ecab44 test(cli): realign opencode-plugin suite to the bare-key static-catalog contract
#9178 (fix #9175) dropped the provider prefix from static-catalog model
dict keys — the correct production behavior (OC's getModel looks models up
by bare id), live-validated in the PR — but the subpackage's own suite was
not swept: 21 tests in config-shim.test.ts + provider-id-routing.test.ts
still asserted the prefixed keys, breaking opencode-plugin CI on every
living-release-PR run since the merge.

- Lookups opencode-omniroute/<raw-id> -> <raw-id>; omniroute/<combo> -> <combo>.
- #7976 anti-double-prefix invariant kept (the negative assert on the
  OC-gate-prefixed key stays).
- Obsolete comment above the raw-model dict write rewritten to describe
  the #9175 contract it contradicted.
- Subpackage lockfile synced to the already-bumped 0.2.1.

Validation: full subpackage suite hermetic — 287/287 pass (was 21 failing).
2026-08-06 10:41:32 -03:00
Diego Rodrigues de Sa e Souza
ece486dc38 fix(resilience): enforce RPM with rolling leases (#9604)
Validated in local merge-train (diegosouzapw batch)
2026-08-06 10:41:04 -03:00
Diego Rodrigues de Sa e Souza
f2e36ad0ce fix(ci): clear base-reds on release/v3.8.50 (migration collision + 4 masked gates) (#9600)
Validated in local merge-train (diegosouzapw batch)
2026-08-06 10:40:56 -03:00
Diego Rodrigues de Sa e Souza
ba0a0751c4 fix(sse): shrink chat.ts model-lockout wiring back under the frozen file-size cap (#9598)
Validated in local merge-train (diegosouzapw batch)
2026-08-06 10:40:46 -03:00
Diego Rodrigues de Sa e Souza
b0cfc3d31c feat(providers): add LLMGateway and LLM Kiwi registries (#9587)
Validated in local merge-train (diegosouzapw batch)
2026-08-06 10:40:38 -03:00
Diego Rodrigues de Sa e Souza
a63940199f feat(providers): add FastRouter AnyAPI and ElectronHub registries (#9586)
Validated in local merge-train (diegosouzapw batch)
2026-08-06 10:40:30 -03:00
Diego Rodrigues de Sa e Souza
bd4407cb64 feat(sse): New-API/One-API/Sub2API aggregator balance detection (#9415) (#9539)
Validated in local merge-train (diegosouzapw batch)
2026-08-06 10:40:20 -03:00
Diego Rodrigues de Sa e Souza
9dc0c6881a feat(opencode-plugin): add visibleModels/hiddenModels allowlist/blocklist (#9473) (#9538)
Validated in local merge-train (diegosouzapw batch)
2026-08-06 10:40:09 -03:00
Diego Rodrigues de Sa e Souza
ce6faa44e5 feat(models): treat quota-exhausted errors as non-hideable in Test All (#9511) (#9537)
Validated in local merge-train (diegosouzapw batch)
2026-08-06 10:40:00 -03:00
Diego Rodrigues de Sa e Souza
2d617325e7 feat(catalog): add hideAutoCombos and hideNoThinkVariants settings toggles (#9418) (#9535)
Validated in local merge-train (diegosouzapw batch)
2026-08-06 10:39:51 -03:00
Diego Rodrigues de Sa e Souza
607bccb6d6 feat(providers): add connection-level custom upstream headers (#8369) (#9497)
Validated in local merge-train (diegosouzapw batch)
2026-08-06 10:39:41 -03:00
Diego Rodrigues de Sa e Souza
a4fbdbffac feat(copilot): add approval gate for runOmniRouteCli commands (#8461) (#9495)
Validated in local merge-train (diegosouzapw batch)
2026-08-06 10:39:27 -03:00
Diego Rodrigues de Sa e Souza
5ea43c7a9d feat: make forwarded upstream response-header budget configurable (#9243) (#9492)
Validated in local merge-train (diegosouzapw batch)
2026-08-06 10:39:15 -03:00
Diego Rodrigues de Sa e Souza
8fdb67f1d3 fix(auth): redirect active sessions from /login (#9491)
Validated in local merge-train (diegosouzapw batch)
2026-08-06 10:39:05 -03:00
Diego Rodrigues de Sa e Souza
53c8016d53 docs: add small VPS memory optimization guide (#8237) (#9471)
Validated in local merge-train (diegosouzapw batch)
2026-08-06 10:38:57 -03:00
Diego Rodrigues de Sa e Souza
b553ac4d14 feat(db): add provider-scoped model aliases (#9068) (#9469)
Validated in local merge-train (diegosouzapw batch)
2026-08-06 10:38:49 -03:00
Diego Rodrigues de Sa e Souza
0720305b38 feat(providers): add Regolo AI provider (#9031) (#9468)
Validated in local merge-train (diegosouzapw batch)
2026-08-06 10:38:41 -03:00
Diego Rodrigues de Sa e Souza
bed6e2b85a feat(infra): add systemd autostart unit for Linux (#8635) (#9466)
Validated in local merge-train (diegosouzapw batch)
2026-08-06 10:38:33 -03:00
Diego Rodrigues de Sa e Souza
3c6f71776e [v3.8.50] fix(sse): replay Gemini thought_signature on direct Claude->Gemini path (400 error)
[v3.8.50] fix(sse): replay Gemini thought_signature on direct Claude→Gemini path (400 error)
2026-08-06 07:00:58 -03:00
diegosouzapw
0afbe3295b Merge branch 'release/v3.8.50' into pr-8755-head 2026-08-06 06:42:02 -03:00
Paijo
2ddbbc61a6 [v3.8.50] feat(memory): MemoryBackend provider pattern with generic HTTP connector (#8752)
Validated in local merge-train T7 (ungrouped batch 2)
2026-08-06 06:06:29 -03:00
Austin Liu
c4527f97bd [v3.8.50] fix(open-sse): add 'has been exhausted' to CREDITS_EXHAUSTED_SIGNALS (fixes #8631) (#8704)
Validated in local merge-train T7 (ungrouped batch 2)
2026-08-06 06:06:19 -03:00
Austin Liu
ae2f7be16f [v3.8.50] fix(errorConfig): add status 499 metadata mapping (fixes #8535) (#8640)
Validated in local merge-train T7 (ungrouped batch 2)
2026-08-06 06:06:09 -03:00
Éder Costa
ea9f15db27 fix: treat zero-reset Antigravity 429s as transient (#8626)
Validated in local merge-train T7 (ungrouped batch 2)
2026-08-06 06:05:58 -03:00
Sean Ford
51f9ffc007 [v3.8.50] feat(services): add Dario as a 5th embedded service (Claude Code toggle/failover) (#8523)
Validated in local merge-train T7 (ungrouped batch 2)
2026-08-06 06:05:46 -03:00
Austin Liu
5dc8631fe4 [v3.8.50] fix(db/apiKeys): respect provider parameter in group model permission checks (fixes #8803) (#8817)
Validated in local merge-train T7 (ungrouped batch 2)
2026-08-06 06:05:39 -03:00
backryun
c3ae5b8893 refactor(db): preserve normalized combo model type (#8809)
Validated in local merge-train T7 (ungrouped batch 2)
2026-08-06 06:05:32 -03:00
Dave
7feafd52c9 [v3.8.50] feat(electron): add Remote Server Mode to attach to an external OmniRoute instance (#8799)
Validated in local merge-train T7 (ungrouped batch 2)
2026-08-06 06:05:24 -03:00
NOXX - Commiter
4a6871381f [v3.8.50] feat(images): add POST /v1/images/upscale (Adobe Firefly Topaz + Stability + Topaz Labs) (#8791)
Validated in local merge-train T7 (ungrouped batch 2)
2026-08-06 06:05:18 -03:00
Diego Rodrigues de Sa e Souza
e7f6b1d130 feat(radar): flag-gated signed free-model catalog overlay (#9515)
* feat(dashboard): add RADAR_ENABLED flag (default off)

* feat(db): radar feed cache + settings with encrypted supporter key

* feat(radar): signed feed sync with pinned key and version floor

- feedSchema.ts: Zod v4 schema mirroring the server feed format
  (discriminated union on budget.kind, enum constraints, etc.)
- pinnedKeys.ts: Ed25519 SPKI-DER pinned key + env override for forks
- verify.ts: signature verification over exact wire bytes, never throws
- sync.ts: full download/verify/validate/cache pipeline with injectable
  deps, feature-flag gate, opt-in gate, version floor (numeric compare),
  and sanitized error reasons (no stack traces)
- 40 tests covering: contract hash, key handling, sig verification,
  schema validation, version compare, all sync paths (disabled, opt_out,
  invalid_signature, invalid_schema, stale, updated, error), auth header
  injection, and cache-untouched assertions for every failure mode

* feat(radar): read-time overlay merge rules over the free catalog

Pure function applyFeed() merges the cached Radar feed over the static
baseline catalog at read time, honoring 4 rules:

1. Feed never overwrites a local override field.
2. enabled:false disables the entry with disabledBy:"radar" provenance.
3. User-added entry NOT in the feed survives untouched.
4. User deletion tombstone prevents feed resurrection.

getRadarCatalog() accessor in index.ts: flag off / no cache / corrupt
payload all fall back to baseline. Valid cache applies the overlay and
returns feed metadata (version, tier, fetchedAt).

TDD: 19 tests (4 rules + dedup + origin + accessor flag/cache/corrupt/
valid/bad-feed + baselineToMergedEntries converter).

* feat(dashboard): radar catalog and guided setup screens

- API routes: GET /api/radar/catalog, POST /api/radar/sync, POST /api/radar/settings
  - All gated on RADAR_ENABLED flag (404 when off)
  - Error responses via buildErrorBody(), never raw stack/message
  - Settings never echoes clear supporter key (masked omr_****<last4>)
  - Sync delegates to syncRadar() server-side, never proxies feed URL
- Dashboard pages:
  - /dashboard/radar: 4 states (flag off, opt-in pending, empty, populated)
  - /dashboard/radar/setup?provider=X: guided setup with steps, key URL, test connection
  - Uses existing Card component and next-intl patterns
- Sidebar: radar entry in costs group with icon
- i18n: pt-BR and en keys for radarPage and radarSetupPage namespaces
- Tests:
  - radar-api-routes.test.ts: 11 tests (flag-off 404, flag-on shape, error sanitization)
  - radar-page-state.test.ts: 5 tests (pure state logic)
  - All 90 radar tests pass (including prior 74)

* docs(radar): module doc and flag-off inertia test

Add docs/frameworks/RADAR.md covering the flag gate, the separate data-sync
opt-in and privacy promise, the Ed25519 signature/pinned-key security model,
tiers, the read-time overlay merge rules, and the self-hosting env vars —
plus index entries in CLAUDE.md/AGENTS.md/docs/README.md/REPOSITORY_MAP.md.

Document RADAR_FEED_URL and RADAR_FEED_PUBKEY in .env.example and
docs/reference/ENVIRONMENT.md to satisfy check:env-doc-sync, which was
failing on this branch since the sync.ts commit added the reads.

Add tests/unit/radar-inertia.test.ts as the single canonical place asserting
the "RADAR_ENABLED off => zero behavioral delta" claim end to end: the three
/api/radar/* routes 404, the flag resolves to the definition default with no
override, getRadarCatalog() returns exactly the baseline without touching the
cache, and computeFreeModelTotals() keeps its pinned values with the Radar
module imported alongside it.

* fix(db): renumber radar migration to 135 after collision with 134

The base branch introduced 134_proxy_logs_egress_ip while this branch carried
134_radar_cache_settings; the migration runner rejects duplicate numeric prefixes.
This migration has never been applied to a real database (the PR is unmerged), so
no retroactive isSchemaAlreadyApplied guard is needed.

* i18n(radar): translate radar catalog and setup strings to all locales

The UI-coverage ratchet measures (present - placeholder) / total_en, so the
__MISSING__ sentinels that i18n:sync-ui writes do not count as covered — only
real translations restore the metric. Scoped to this PR's namespaces
(radarPage, radarSetupPage, sidebar.radar*) instead of a bulk sync, which would
have pulled ~978 unrelated pending keys into this diff.

Placeholders and code identifiers verified preserved across all 1682 strings.

* fix(radar): trust the served-tier header instead of the signed body field

The signed feed body always carries tier:"live" by design (one signed
artifact per version — rewriting the field server-side per request
would break the exact-bytes Ed25519 signature). The server now returns
the tier ACTUALLY served via the x-omniroute-feed-tier response
header, so free users on a delayed community snapshot no longer see
"Ao vivo (tempo real)" in the UI.

sync.ts now reads and validates that header (falling back to the
body's tier only when the header is absent or holds an unrecognized
value) and stores the served tier in the cache; index.ts already
surfaces cache.tier to the UI unchanged.

* test(combo): shorten an assert message that exceeded the line limit

The assertion added by #9507 was 104 chars, so prettier reformatted it into
five lines on the next commit that touched the file, pushing it past its
frozen size (3449) and failing check:file-size. The message is shortened
(the issue reference stays in the comment directly above); the assertion
itself is unchanged, and the file is back to 3448 lines and prettier-clean.

* i18n(radar): use the canonical zh-TW glossary terms

The machine translation produced retired renderings the glossary gate blocks:
供應商 for provider (canonical 提供者) and 文檔 for documentation (canonical 文件).
Fixed across the 11 affected radar strings; tests/unit/i18n-glossary-consistency-check.test.ts
is back to 17/17.

* fix(radar): point the default feed URL at the domain that exists

radar.omniroute.dev was a placeholder for a domain that was never registered,
so an out-of-the-box sync would fail DNS resolution for every user. The live
feed is served from radar.omniroute.online (the subdomain the design always
specified), now behind Cloudflare TLS. Forks still override it via
RADAR_FEED_URL.

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-06 05:58:58 -03:00
Austin Liu
8a7b2467fd [v3.8.50] feat(combo): add maxContextWindow to contextRequirements (fixes #8777) (#8790)
Validated in local merge-train T6 (ungrouped batch 1)
2026-08-06 05:24:39 -03:00
Mohit Rawat
a65887eec0 fix(test): revive orphaned open-sse vitest tests (#8772)
Validated in local merge-train T6 (ungrouped batch 1)
2026-08-06 05:24:33 -03:00
Austin Liu
f843bac030 [v3.8.50] fix(auth): accept x-api-key without anthropic-version for claude-code user-agent (fixes #8655) (#8678)
Validated in local merge-train T6 (ungrouped batch 1)
2026-08-06 05:24:27 -03:00
Éder Costa
0b4bc4f4b1 [v3.8.50] fix(antigravity): lock full quota per exact model (#8630)
Validated in local merge-train T6 (ungrouped batch 1)
2026-08-06 05:24:21 -03:00
Éder Costa
aa7b391386 fix(claude): preserve signed thinking turns during obfuscation (#8629)
Validated in local merge-train T6 (ungrouped batch 1)
2026-08-06 05:24:13 -03:00
backryun
011a404684 fix(types): preserve browser abort handling (#8818)
Validated in local merge-train T6 (ungrouped batch 1)
2026-08-06 05:24:00 -03:00
Prudhvi Vuda
7c0dba222c [v3.8.50] fix(sse): stop fabricating encrypted Codex reasoning summary text (#8807)
Validated in local merge-train T6 (ungrouped batch 1)
2026-08-06 05:23:53 -03:00
Alberto Punter
39220a1a5c fix(i18n): correct and naturalize Spanish translations (#8339)
Validated in local merge-train T6 (ungrouped batch 1)
2026-08-06 05:23:45 -03:00
Diego Rodrigues de Sa e Souza
04683029a6 fix(build): exec native esbuild binary directly in prepublish (dast-smoke base-red) (#9558)
* fix(build): exec native tool binaries directly in runBuildTool

#8858 routed every resolved local bin through process.execPath to avoid
Windows .cmd shims — but esbuild >=0.25 ships bin/esbuild as the NATIVE
platform executable (ELF on Linux), so Node parsed machine code as JS and
build:cli died with 'SyntaxError: Invalid or unexpected token', turning
dast-smoke red for every PR.

runBuildTool now sniffs the entry's magic bytes (ELF / Mach-O / PE) and
execs native binaries directly; JS entries keep going through this Node
binary (the .cmd-shim avoidance #8858 wanted).

Validation (RED->GREEN on this box):
- RED: node node_modules/esbuild/bin/esbuild --version -> SyntaxError (ELF)
- GREEN: the exact failing CI step reproduced via the new logic bundles
  open-sse/mcp-server/server.ts successfully (4.2MB output, 1.3s).

* fix(docs): add MDX frontmatter to the 20 remaining docs without it

Same failure class as AGENTROUTER_WAF (#9503) and DOCKER_RELEASE_CHANNELS
(this run's dast-smoke red): any doc without frontmatter breaks the
fumadocs MDX loader during next build, killing build:cli/dast-smoke for
every PR. Swept ALL of docs/ (i18n mirrors excluded) in one pass so this
class cannot recur one file at a time.

* docs(env): document OMNIROUTE_INTERNAL_SERVICE_TOKEN(+_FILE), OPENROUTER_PROVIDER_STATS_* and embedded-Redis binding vars

Pre-existing env/docs contract drift from recently merged features made
check:env-doc-sync red for any docs-touching PR. Values and defaults read
from the defining modules (internalServiceAuth.ts, openrouterProviderStats.ts).

* fix(build): resolve bundled npm-cli.js in the standard Unix layout + safe npm fallback off-Windows

The opencode-plugin step hard-failed on GitHub runners because
resolveBundledNpmEntry only looked next to the node binary (Windows zip
layout); hostedtoolcache Node keeps npm at <prefix>/lib/node_modules/npm.
Added that candidate, and when neither exists on non-Windows the step now
falls back to plain 'npm' — the .cmd-shim hazard #8858 avoids is
Windows-only.

* test(mutation): register xai-agent-tools-passthrough.test.ts in stryker tap.testFiles

The test landed on release/v3.8.50 covering
open-sse/handlers/chatCore/passthroughHelpers.ts without the stryker
registration, so Fast Quality Gates' drift detection reds any PR that
carries it. Mechanical registration so its mutant kills count.

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-06 02:19:57 -03:00
Diego Rodrigues de Sa e Souza
a33fb7c4e6 fix(mcp): give the audit tests a loader seam createRequire cannot hide from (#9559)
* fix(mcp): give the audit tests a loader seam createRequire cannot hide from

Since #8959 the audit DB loads better-sqlite3 via createRequire() (so
Electron/global-install resolution works) — which vi.doMock cannot
intercept: it only patches Vitest's ESM module graph. The audit.test.ts
better-sqlite3 mock therefore never engaged; the tests opened a REAL
empty sqlite file in the temp DATA_DIR ('no such table: mcp_tool_audit'
on stderr) and every mock assertion counted zero calls. The 3 failures
are deterministic (reproduced 3/3 locally), redding Vitest (fast-path)
for the entire PR queue — long misdiagnosed as a flake (#9095 merge
notes call it 'pre-existing audit.test.ts flake').

- Shutdown tests inject the mock through the audit connection cache
  (globalThis.__omnirouteMcpAuditDb) — the module's own seam.
- The node:sqlite fallback test drives __setBetterSqliteLoaderForTests,
  a test-only loader override; the production createRequire path is
  untouched (node:sqlite itself is import()'d, so its doMock still
  works).

3/3 red -> 3/3 green; full open-sse/mcp-server vitest suite 88/88.

* chore: align changelog slug with the PR number (9559)

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-06 01:45:43 -03:00
Diego Rodrigues de Sa e Souza
8c5bfbe631 fix(quality): reconcile inherited file-size drift on the release tip (#9554)
* fix(quality): reconcile inherited file-size drift on the release tip

13 files sit above their frozen LOC on the clean tip 8180b49ce1
(measured by the gate itself). The PR-mode base-relative check (#8522)
correctly lets innocent PRs pass, but per-PR rebaselines were lost
across successive conflict resolutions of this hot file during the
08-05/06 merge batch — so the absolute mode (nightly, local runs) is
permanently red and stops distinguishing real growth from inherited
drift.

Frozen values updated to the measured tip, each annotated with the
merged PR that grew the file (#9024 #9324 #9329 #9193 #9332 #9228
#9236 #9314 #9260 #8934 #9196 #9163); executors default.ts and kiro.ts
(above the 1000 cap with no frozen entry) join the frozen set.

* fix(quality): prune orphaned ESLint suppressions and clear the 5 unsuppressed errors

The 'No new ESLint warnings' job reds the whole queue with exit 2:
'There are suppressions left that do not occur anymore' — the 08-05
merge batch removed code whose violations were frozen in
eslint-suppressions.json, leaving orphaned entries (673->670 files,
4338->4333 violations after eslint --prune-suppressions).

The full-tree run also surfaced 5 real unsuppressed errors merged with
the batch, fixed here instead of suppressed (new violations must be
fixed, per policy): 4x no-explicit-any in
tests/unit/catalog-order-contract.test.ts ((conn as any).id -> typed
cast) and 1x react/no-unescaped-entities in the agent-bridge
SetupWizard (#9095).

Also restores the _comment policy header the successive hot-file
conflict resolutions had dropped (TS7 debt freeze provenance + prune
policy).

* fix(quality): absorb the two file-size growths merged while this PR was in CI

The base kept moving during the reconcile cycle: #9184 grew
src/sse/handlers/chat.ts 1857->1877 and #9005 grew
open-sse/executors/default.ts 1027->1042. Re-measured on the merged
tree; gate back to 0 violations.

* fix(tests): move the orphaned RTL ratchet test to a collected path as node:test

#8828 added tests/unit/scripts/check-rtl-ratchet.test.ts — a path no
runner collects (the node:test globs enumerate an explicit subdir list
without scripts/, and vitest.config.ts never included it), so the file
NEVER ran and the test-discovery orphan gate reds the queue. Moved to
tests/unit/ (collected by node:test) and converted from vitest
describe/it/expect to node:test+assert to match the runner and the
sibling check-*.test.ts files. 5/5 green under the real runner.

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-06 01:45:28 -03:00
Diego Rodrigues de Sa e Souza
e12d2d546b fix(build): resolve npm-cli.js on POSIX layouts in the shim-free prepublish resolver (#9553)
* fix(build): resolve npm-cli.js on POSIX layouts in the shim-free prepublish resolver

The #8858 resolver only tried <dir(node)>/node_modules/npm/bin — the
Windows layout. On POSIX (GitHub hosted runners, nvm, system installs)
npm lives at <prefix>/lib/node_modules/npm while node is <prefix>/bin/
node, so resolveBundledNpmEntry returned null and npm run build:cli
died installing @omniroute/opencode-plugin deps on every fresh checkout
('npm-cli.js not found next to the running Node binary') — redding Fast
Production Build and dast-smoke for the whole PR queue.

Extract the resolver to scripts/build/resolveNpmEntry.ts with injectable
seams and try, in order: npm_execpath (exported by npm run itself), the
Windows beside-the-binary layout, the POSIX <prefix>/lib layout.

TDD: tests/unit/build/resolve-npm-entry.test.ts — the POSIX-layout and
npm_execpath cases plus a live regression guard fail against the old
single-candidate logic (2/5) and pass with the fix (5/5).

* docs(env): register the 7 env vars orphaned by the 08-05 merge batch

The Docs Gates env/docs contract went red on the release tip: #9260
added OMNIROUTE_INTERNAL_SERVICE_TOKEN(_FILE) and #9324 added
OPENROUTER_PROVIDER_STATS_ENABLED/_TTL_MS without .env.example entries,
and the #9286 Redis sidecar vars (REDIS_BIND_HOST, REDIS_PORT,
OMNIROUTE_REDIS_BIND_HOST) never reached ENVIRONMENT.md. Inherited
base-red on every open PR. Defaults and descriptions taken from the
consuming source files.

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-06 01:13:42 -03:00
Paco Cartones
bca61af8ae fix(api/skills): sanitize error messages before returning them to clients (#9088)
Validated in local merge-train T5 (base49+contributors+pacocartones)
2026-08-06 00:09:55 -03:00
Paco Cartones
7d07f1f500 fix(a2a): timing-safe bearer comparison + drop per-request debug log (#9083)
Validated in local merge-train T5 (base49+contributors+pacocartones)
2026-08-06 00:09:49 -03:00
Paco Cartones
7c0a96c69b fix(batches): validate the list endpoint's ?limit query param (#9073)
Validated in local merge-train T5 (base49+contributors+pacocartones)
2026-08-06 00:09:43 -03:00
Paco Cartones
fda66315ad test(sse): cover the 14 exported specificity-rules detectors (#9063)
Validated in local merge-train T5 (base49+contributors+pacocartones)
2026-08-06 00:09:37 -03:00
NOXX - Commiter
a072ff4552 fix(adobe-firefly): open browser sign-in and resolve provider slug in /login (#9097)
Validated in local merge-train T5 (base49+contributors+pacocartones)
2026-08-06 00:09:30 -03:00
Marcelo Teixeira Monteiro
754782f620 fix(deps): resolve merge conflict in Redis security update (#9065)
Validated in local merge-train T5 (base49+contributors+pacocartones)
2026-08-06 00:09:24 -03:00
3g0r1ch
065301f79d feat(model-alias): add runtime Model Alias Resolver middleware (#9020)
Validated in local merge-train T5 (base49+contributors+pacocartones)
2026-08-06 00:09:18 -03:00
Curtastrophe
1a8f10bf67 fix(executors): backfill missing tool message names for Kimi K3 and strict BYOK providers (#9005)
Validated in local merge-train T5 (base49+contributors+pacocartones)
2026-08-06 00:09:11 -03:00
3g0r1ch
07cd4c0f2e i18n(ru): complete Russian locale — fill 25 missing keys and 80 placeholders (#9001)
Validated in local merge-train T5 (base49+contributors+pacocartones)
2026-08-06 00:09:05 -03:00
Rahul sharma
a9b4c3efff fix(api): alias-backed models leak raw node UUID prefix in /v1/models (#8958) (#8961)
Validated in local merge-train T5 (base49+contributors+pacocartones)
2026-08-06 00:08:59 -03:00
Ahmet Çetinkaya
749fc75beb fix(combo): exclude hidden leaves from catalog and dispatch (#8878)
Validated in local merge-train T5 (base49+contributors+pacocartones)
2026-08-06 00:08:52 -03:00
Tech Guy
9751821338 chore(quality): add an RTL layout ratchet (#8828)
Validated in local merge-train T5 (base49+contributors+pacocartones)
2026-08-06 00:08:46 -03:00
Austin Liu
8ddb881412 fix(backend): remove dead model IDs and update active models for Cloudflare Workers AI (#8717) (#8808)
Validated in local merge-train T5 (base49+contributors+pacocartones)
2026-08-06 00:08:40 -03:00
Anjielon
4b2ac70295 feat(compression): add Italian (it) Caveman rule pack (#8776)
Validated in local merge-train T5 (base49+contributors+pacocartones)
2026-08-06 00:08:35 -03:00
PizzaV
335829858b feat(dahl): add manual API key option alongside auto-generated token (#9077)
Validated in local merge-train T5 (base49+contributors+pacocartones)
2026-08-06 00:08:29 -03:00
Mauricio Antonio Sevilla Britto
ad82c81c38 fix(build): support npm v11 allowScripts for optional native deps (#8877)
Validated in local merge-train T5 (base49+contributors+pacocartones)
2026-08-06 00:08:23 -03:00
NOXX - Commiter
4f89f2b7bf fix(adobe-firefly): cap gpt-image refs at 2 + adaptive poll timeout (#8870)
Validated in local merge-train T4 (HouMinXi+Zartharas+Andrian+artickc)
2026-08-05 23:52:44 -03:00
Andrew B.
63c484d062 fix(usage): aggregate provider window costs in SQL (#8892)
Validated in local merge-train T4 (HouMinXi+Zartharas+Andrian+artickc)
2026-08-05 23:52:38 -03:00
Andrew B.
bf37618a61 fix(deepseek-web): enable toolCalling on all models (#8889)
Validated in local merge-train T4 (HouMinXi+Zartharas+Andrian+artickc)
2026-08-05 23:52:32 -03:00
Andrew B.
48b5c7fb81 fix(sse): use brand-neutral keepalive placeholders (#8888)
Validated in local merge-train T4 (HouMinXi+Zartharas+Andrian+artickc)
2026-08-05 23:52:27 -03:00
Aman
a1edde420e fix(stream): preserve standalone whitespace deltas (#9189)
Validated in local merge-train T4 (HouMinXi+Zartharas+Andrian+artickc)
2026-08-05 23:52:21 -03:00
Aman
e1eaf0cc79 fix(health): skip disabled provider connections (#9186)
Validated in local merge-train T4 (HouMinXi+Zartharas+Andrian+artickc)
2026-08-05 23:52:15 -03:00
Aman
d2f3c1abf5 fix(docker): bundle LLMLingua optional dependencies (#9185)
Validated in local merge-train T4 (HouMinXi+Zartharas+Andrian+artickc)
2026-08-05 23:52:10 -03:00
Aman
def958b97a fix(routing): evict affinity after terminal stream EOF (#9184)
Validated in local merge-train T4 (HouMinXi+Zartharas+Andrian+artickc)
2026-08-05 23:52:04 -03:00
Aman
9ef7d9cf97 fix(settings): allow hidePaidModels updates (#9182)
Validated in local merge-train T4 (HouMinXi+Zartharas+Andrian+artickc)
2026-08-05 23:51:58 -03:00
Aman
1b2a72ebc8 feat(docker): publish next from active release branches (#9181)
Validated in local merge-train T4 (HouMinXi+Zartharas+Andrian+artickc)
2026-08-05 23:51:50 -03:00
Bob.Hou
76e127beb1 fix(sse): default OpenAI Chat Completions to non-stream when stream omitted (#8976)
Validated in local merge-train T4 (HouMinXi+Zartharas+Andrian+artickc)
2026-08-05 23:51:43 -03:00
Bob.Hou
ff012ff420 fix(antigravity): add onboardUser fallback when loadCodeAssist returns no project (#8886)
Validated in local merge-train T4 (HouMinXi+Zartharas+Andrian+artickc)
2026-08-05 23:51:36 -03:00
diegosouzapw
84ab6fa7b0 test(sse): add thoughtSignature assertion for Gemini direct path (#3440)
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-08-04 08:33:22 -03:00
diegosouzapw
14014fda12 Merge branch 'claude-to-gemini-400-error-fix' of https://github.com/csoftware-arigpt/OmniRoute into fix/pr-8755-thought-signature 2026-08-04 08:27:04 -03:00
csoftware-arigpt
30cf91e272 Preserve #3440 coverage under signature replay
Seed a connection-scoped Gemini thought signature before exercising the Claude-to-Gemini id assertions. This keeps the regression focused on the public-Gemini versus Vertex id contract while honoring the new signature replay behavior.

Constraint: Signature-less historical Claude tool calls are intentionally converted to context text.

Rejected: Emit unsigned functionCall parts in the fixture | Gemini 3+ rejects that production behavior.

Confidence: high

Scope-risk: narrow

Tested: Prettier, repository ESLint hook, vertex-functioncall-id-3440 Node test, git diff --check

Not-tested: Full repository test suite
2026-07-27 20:18:20 +03:00
csoftware-arigpt
c30724742d fix(sse): replay Gemini thought_signature on direct Claude→Gemini path (#2504)
Direct Claude↔Gemini translator was missing the thought_signature round-trip
that the OpenAI-hub path already had (#2504). Gemini 3+ thinking models strictly
validate thought_signature on every functionCall part in a multi-turn tool-call
batch and return HTTP 400 ("Function call is missing a thought_signature in
functionCall parts") when it is absent — breaking all agentic workflows through
Claude Code.

Three fixes across the direct path:

1. translator/index.ts — thread the per-connection signature namespace
   (connectionId) into the direct-path credentials, mirroring the hub path.
   Without this, claudeToGeminiRequest never receives _signatureNamespace and
   cannot look up stored signatures.

2. translator/response/gemini-to-claude.ts — capture thoughtSignature from
   Gemini response parts (functionCall, thought, or standalone signature parts)
   via state.pendingThoughtSignature, and persist it keyed by
   buildGeminiThoughtSignatureKey(connectionId, toolId). The signature
   frequently lands on a preceding thought part rather than the functionCall
   itself, so pending tracking across stream chunks is required.

3. translator/request/claude-to-gemini.ts — for each tool_use block, resolve the
   stored signature and attach it as thoughtSignature on the functionCall part.
   When no signature is available (historical tool calls predating the store, or
   cold-start), omit the functionCall part entirely and convert the matching
   tool_result to plain text — mirrors openai→gemini context mode, avoiding the
   bare-functionCall 400 while preserving conversation context.
2026-07-27 14:25:16 +03:00
997 changed files with 67616 additions and 14180 deletions

View File

@@ -0,0 +1 @@
- **docs:** add management authentication terminology guide ([#7786](https://github.com/diegosouzapw/OmniRoute/issues/7786))

View File

@@ -0,0 +1,41 @@
# Management Authentication
OmniRoute uses four distinct credential families for management access. This guide
distinguishes them by purpose, scope, and locality.
| Credential | Scope | Locality | Use Case |
|-------------------------|--------------------|---------------|-----------------------------------|
| Dashboard JWT session | Full management | Localhost | Web dashboard login |
| CLI machine-id token | Full management | Per-machine | `omniroute` CLI commands |
| Scoped `oma_` token | Configurable scope | External | Automation / CI / API access |
| Manage-scope API key | `manage` scope | External | Management API calls |
## Dashboard JWT Session
Generated on dashboard login (`/api/auth/login`). Stored in HTTP-only cookie.
Valid for the session duration. Cannot be used from external hosts.
## CLI Machine-ID Token
Created by `omniroute auth login` on first use. Stored in `~/.omniroute/auth.json`.
Used by the CLI for all management operations. Tied to the machine identity.
## Scoped `oma_` Access Token
Created via dashboard or CLI with configurable scopes (e.g., `manage`, `read`).
Format: `oma_<random-hex>`. Used for programmatic access from external systems.
## Manage-Scope API Key
Standard API key with the `manage` scope enabled. Created in dashboard API Keys page.
Used for management API calls from external hosts.
## Header Examples
```
Authorization: Bearer oma_abc123def456
Authorization: Bearer <standard-api-key-with-manage-scope>
Cookie: omniroute_session=<jwt-token>
```
See `docs/reference/API_REFERENCE.md` for endpoint-specific auth requirements.

View File

@@ -0,0 +1,27 @@
import { describe, it } from "node:test";
import { ok } from "node:assert/strict";
import { readFileSync } from "node:fs";
describe("Management auth documentation (#7786)", () => {
const docPath = "docs/guides/MANAGEMENT-AUTH.md";
const content = readFileSync(docPath, "utf-8");
it("exists and has content", () => {
ok(content.length > 500, "should have substantial content");
ok(content.includes("Dashboard JWT session"));
ok(content.includes("CLI machine-id token"));
ok(content.includes("oma_"));
});
it("documents all four credential families", () => {
const families = ["Dashboard JWT", "CLI machine-id", "oma_", "Manage-scope"];
for (const f of families) {
ok(content.includes(f), `should document ${f}`);
}
});
it("mentions relevant auth header examples", () => {
ok(content.includes("Authorization"));
ok(content.includes("Bearer"));
});
});

View File

@@ -0,0 +1 @@
- **feat(infra):** add systemd autostart unit for Linux ([#8635](https://github.com/diegosouzapw/OmniRoute/issues/8635))

View File

@@ -0,0 +1,19 @@
[Unit]
Description=OmniRoute AI Proxy
After=network.target network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=$(which omniroute) start
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production
# Security hardening
NoNewPrivileges=true
ProtectSystem=full
PrivateTmp=true
[Install]
WantedBy=default.target

View File

@@ -0,0 +1,23 @@
import { describe, it } from "node:test";
import { ok } from "node:assert/strict";
import { readFileSync, existsSync } from "node:fs";
describe("Systemd autostart (#8635)", () => {
const svcPath = "contrib/systemd/omniroute.service";
const content = readFileSync(svcPath, "utf-8");
it("service file exists", () => {
ok(existsSync(svcPath));
ok(content.length > 200);
});
it("defines required systemd sections", () => {
ok(content.includes("[Unit]"));
ok(content.includes("[Service]"));
ok(content.includes("[Install]"));
});
it("specifies WantedBy=default.target", () => {
ok(content.includes("WantedBy=default.target"));
});
});

View File

@@ -24,6 +24,7 @@ coverage
# Runtime data and logs
data
logs
.sandbox
# Local env files (inject at runtime via --env-file or -e)
.env

View File

@@ -0,0 +1,6 @@
ENABLE_LIVE_DEVIN_TESTS=0
DEVIN_BRIDGE_MODEL=devin-cli-agentic/swe-1-7
DEVIN_BRIDGE_SONNET_MODEL=devin-cli-agentic/swe-1-7
DEVIN_BRIDGE_OPUS_MODEL=devin-cli-agentic/swe-1-7
DEVIN_BRIDGE_HAIKU_MODEL=devin-cli-agentic/swe-1-7
DEVIN_BRIDGE_SUBAGENT_MODEL=devin-cli-agentic/swe-1-7

View File

@@ -345,14 +345,18 @@ ALLOW_API_KEY_REVEAL=false
# OMNIROUTE_CHAT_HEAVY_TOOL_COUNT=64
# Conservative string-size token estimate that classifies a request as heavyweight. Default 32000.
# OMNIROUTE_CHAT_HEAVY_ESTIMATED_TOKENS=32000
# Hard message-count cap; excess receives compact-required 413. Default 800.
# OMNIROUTE_CHAT_HARD_MAX_MESSAGES=800
# Optional opt-in hard message-count cap; excess receives compact-required 413 before
# compression can run. Unset/0 (the default) means no history cap: heap growth is bounded
# by OMNIROUTE_CHAT_MAX_HEAVY_IN_FLIGHT and the heap-pressure shed instead. Set a positive
# value only on memory-constrained deployments that need a hard ceiling.
# OMNIROUTE_CHAT_HARD_MAX_MESSAGES=0
# Hard cap (bytes) for a non-streaming upstream response buffered fully into memory
# (#5152). Past this the upstream reader is cancelled and the request fails fast
# instead of growing an unbounded string until the V8 heap is exhausted.
# Used by: open-sse/handlers/chatCore/nonStreamingResponseBody.ts
# Default: 67108864 (64 MB)
# OMNIROUTE_FORWARDING_HEADER_BUDGET_BYTES=768
# OMNIROUTE_MAX_NONSTREAMING_RESPONSE_BYTES=67108864
# CORS configuration — controls which cross-origin browser clients can call the API.
@@ -793,6 +797,16 @@ PROVIDER_LIMITS_SYNC_SPACING_MS=1500
# Disable the proactive recovery scheduler entirely (default: false).
# OMNIROUTE_DISABLE_CONNECTION_RECOVERY=false
# Proactive Claude warmup scheduler (#8848): fires a trivial request to opted-in
# OAuth connections on a cron schedule (America/Los_Angeles) so accounts do not
# hit the 5-hour sliding window cold. Off by default — set ENABLED=1 and flip
# per-connection flags in settings.claudeWarmup.connections to activate.
# Used by: src/lib/warmupScheduler.ts.
# OMNIROUTE_WARMUP_ENABLED=false
# OMNIROUTE_WARMUP_CRON="0 7 * * *"
# OMNIROUTE_WARMUP_CONCURRENCY=3
# OMNIROUTE_WARMUP_MODEL=
# Background job interval for budget reset checks (ms). Default: 600000 (10m).
# Used by: src/lib/jobs/budgetResetJob.ts. Floor: 10000.
#OMNIROUTE_BUDGET_RESET_JOB_INTERVAL_MS=600000
@@ -857,6 +871,12 @@ PROVIDER_LIMITS_SYNC_SPACING_MS=1500
# (>= 3 retrievals = never compressed). 1 disables the ramp (binary skip at the threshold only).
# Used by: open-sse/services/compression/engines/ccr/index.ts. Default: 2.
#COMPRESSION_CCR_RETRIEVAL_RAMP_FACTOR=2
# CCR durable block store (#9061). The in-memory store loses blocks to LRU eviction, the TTL, a
# restart, or a retrieve landing on another instance, while the model is told it can retrieve them
# verbatim. Set to false to keep blocks in memory only, at the cost of that promise. Blocks over
# 512KB and cloud runtimes are memory-only regardless.
# Used by: open-sse/services/compression/engines/ccr/index.ts. Default: true.
#COMPRESSION_CCR_DURABLE_STORE=true
# T08/H5 — usage-observed prefix freeze (OPT-IN, default off). When enabled, a system prompt seen
# >= THRESHOLD times is treated as a stable cacheable prefix and preserved from compression even
# for providers the static cache-aware heuristic does not recognize (freeze = preserve, never
@@ -1041,6 +1061,17 @@ GITHUB_OAUTH_CLIENT_ID=Iv1.b507a08c87ecfe98
# VISION_BRIDGE_BASE_URL=
# VISION_BRIDGE_API_KEY=
# ── Raycast Pro (local auto-import) ──
# Raycast Pro AI is a reverse-engineered, unofficial API — local/personal use
# only (no OAuth client_id/secret; token is captured via macOS Auto-Import
# from the Keychain + local Raycast SQLite DB, or pasted manually). These
# vars are optional manual overrides used by open-sse/services/raycast.ts
# and the direct-probe benchmark script scripts/raycast/usage-benchmark.mjs.
# RAYCAST_BEARER_TOKEN=
# RAYCAST_DEVICE_ID=
# RAYCAST_AID=
# RAYCAST_SIG_SECRET=
# ─────────────────────────────────────────────────────────────────────────────
# ⚠️ GOOGLE OAUTH (Antigravity) & OTHER PROVIDERS — REMOTE SERVERS
# ─────────────────────────────────────────────────────────────────────────────
@@ -1253,6 +1284,14 @@ CURSOR_USER_AGENT="Cursor/3.4"
# OMNIROUTE_BROWSER_POOL=on
# WEB_COOKIE_USE_BROWSER=0
# ── Adobe Firefly browser sign-in (system Chrome/Edge CDP) ──
# Used by: open-sse/services/adobeFireflyBrowserLogin.ts. The Firefly login
# flow drives a real, system-installed Chrome or Microsoft Edge via CDP so the
# user can sign in interactively; the executable is auto-detected from common
# install paths per OS. Set this to override that detection (e.g. a portable
# install or a non-standard path) when auto-detection fails.
# OMNIROUTE_LOGIN_BROWSER_PATH=
# ── Circuit breaker thresholds and reset windows ──
# Used by: open-sse/config/constants.ts → src/lib/resilience/settings.ts.
# Defaults match historical PROVIDER_PROFILES values (post-scaling for
@@ -1501,6 +1540,15 @@ APP_LOG_TO_FILE=true
# ═══════════════════════════════════════════════════════════════════════════════
# 19. MODEL SYNC (Dev)
# ═══════════════════════════════════════════════════════════════════════════════
# Enable the models.dev capability sync. Default: false (opt-in only).
# Also settable from Dashboard > Settings > AI. This variable wins over that
# setting whenever it is set to anything non-empty, in either direction, so a
# deployment can pin the sync on or off without depending on database state
# surviving a rebuild. Leave it unset to let the dashboard toggle decide.
# On: 1, true, yes or on (any casing). Any other value is off.
# Used by: src/lib/modelsDevSync.ts
# MODELS_DEV_SYNC_ENABLED=false
# Development-time model catalog sync interval in seconds.
# Used by: src/lib/modelsDevSync.ts
# Default: 86400 (24 hours)
@@ -1523,6 +1571,14 @@ APP_LOG_TO_FILE=true
# Default: 86400000 (24 hours)
# OPENROUTER_CATALOG_TTL_MS=86400000
# Enrich the dashboard providers list with OpenRouter weekly ranking stats.
# ON by default; set false to skip the background fetch entirely (#9324).
# Used by: src/lib/catalog/openrouterProviderStats.ts
# OPENROUTER_PROVIDER_STATS_ENABLED=true
# Cache TTL for the OpenRouter provider stats snapshot, in ms.
# Default: 86400000 (24 hours)
# OPENROUTER_PROVIDER_STATS_TTL_MS=86400000
# ── Model catalog response shape ──
# Include display-friendly name fields in /v1/models responses.
# Disable for clients that expect model IDs only.
@@ -1543,6 +1599,13 @@ APP_LOG_TO_FILE=true
# DESIGNER_WEB_POLL_TIMEOUT_MS=60000 # Max wait for job completion (default: 60s)
# DESIGNER_WEB_POLL_INTERVAL_MS=2000 # Poll frequency (default: 2s)
# ── Adobe Firefly (Image Upscale) ──
# Base delay (ms) for the submit-retry exponential backoff when Adobe Firefly's
# upscale job submission is rate-limited. Used by:
# open-sse/services/adobeFireflyUpscale.ts::submitRetryDelayMs.
# Default: 8000 (20 under NODE_ENV=test/VITEST/NODE_TEST_CONTEXT).
# ADOBE_FIREFLY_SUBMIT_BASE_DELAY_MS=8000
# ── AWS Bedrock (Kiro / Audio) ──
# Region used to construct AWS Bedrock endpoints. Used by:
# src/lib/providers/validation.ts and open-sse/handlers/audioSpeech.ts.
@@ -1637,6 +1700,26 @@ APP_LOG_TO_FILE=true
# Used by: src/lib/services/bootstrap.ts, src/app/api/services/mux/_lib.ts
# MUX_SERVICE_PORT=8322
# ── Dario embedded service ──
# Override the host/port the embedded Dario (Claude Code subscription proxy)
# daemon binds to and is reached at. Always bound to 127.0.0.1 — never
# configurable to 0.0.0.0. Rarely needed — defaults to 127.0.0.1:3456.
# Used by: src/lib/services/installers/dario.ts, src/lib/services/bootstrap.ts,
# src/app/api/services/dario/_lib.ts, src/app/api/services/dario/admin/_lib.ts,
# open-sse/executors/dario.ts
# DARIO_HOST=127.0.0.1
# DARIO_PORT=3456
# ── Dario embedded service ──
# Override the host/port the embedded Dario (Claude Code subscription proxy)
# daemon binds to and is reached at. Always bound to 127.0.0.1 — never
# configurable to 0.0.0.0. Rarely needed — defaults to 127.0.0.1:3456.
# Used by: src/lib/services/installers/dario.ts, src/lib/services/bootstrap.ts,
# src/app/api/services/dario/_lib.ts, src/app/api/services/dario/admin/_lib.ts,
# open-sse/executors/dario.ts
# DARIO_HOST=127.0.0.1
# DARIO_PORT=3456
# ── Local hostnames (Docker networking) ──
# Comma-separated additional hostnames treated as "local" for provider routing.
# Used by: open-sse/config/providerRegistry.ts — allows Docker service names.
@@ -1869,6 +1952,18 @@ APP_LOG_TO_FILE=true
# ── Devin CLI binary path ──
# Used by: open-sse/executors/devin-cli.ts. Default: looked up via PATH.
# CLI_DEVIN_BIN=devin
# Agentic bridge-only binary override. The bridge still executes ACP stdio only.
# CLI_DEVIN_AGENTIC_BIN=devin
# Required isolated HOME for the agentic Devin child process.
# DEVIN_AGENTIC_HOME=/home/bridge
# Bounded ACP turn timeout in milliseconds. Default: 120000.
# DEVIN_AGENTIC_ACP_TIMEOUT_MS=120000
# Agentic bridge model aliases. Values must keep the devin-cli-agentic/ prefix.
# DEVIN_BRIDGE_MODEL=devin-cli-agentic/swe-1-7
# DEVIN_BRIDGE_SONNET_MODEL=devin-cli-agentic/swe-1-7
# DEVIN_BRIDGE_OPUS_MODEL=devin-cli-agentic/swe-1-7
# DEVIN_BRIDGE_HAIKU_MODEL=devin-cli-agentic/swe-1-7
# DEVIN_BRIDGE_SUBAGENT_MODEL=devin-cli-agentic/swe-1-7
# ── Command Code (custom CLI) callback ──
# Local port used for OAuth-style callbacks from the Command Code CLI helper.
@@ -2139,6 +2234,11 @@ PLAYGROUND_COMPARE_MAX_COLUMNS=4
# MEMORY_TYPED_DECAY_EPISODIC_DAYS=30 # episodic TTL in days; 0 = episodic immune too
# MEMORY_TYPED_DECAY_ACCESS_IMMUNITY=3 # access_count >= N → immune; 0 disables access immunity
# MEMORY_TYPED_DECAY_SWEEP_INTERVAL=0 # periodic sweep interval (seconds); 0 = no periodic sweep
# ─── Memory Backend Connectors (Generic HTTP) ──────────────────────────────
# NOTION_API_KEY=
# NOTION_API_URL=
# OBSIDIAN_API_KEY=
# OBSIDIAN_API_URL=
# AgentBridge + Traffic Inspector (Group A)
# AgentBridge
@@ -2154,6 +2254,15 @@ INSPECTOR_MAX_BODY_KB=1024
INSPECTOR_MASK_SECRETS=true
INSPECTOR_LLM_HOSTS_EXTRA=
INSPECTOR_INTERNAL_INGEST_TOKEN=
# Shared secret for identity-preserving internal REST hops (#9260): when an
# OmniRoute component calls another local OmniRoute route, this token (sent as
# x-omniroute-internal-service-token) marks the request as internal so the
# original caller identity is preserved. OPT-IN: unset disables the mechanism.
# Used by: src/lib/api/internalServiceAuth.ts
# OMNIROUTE_INTERNAL_SERVICE_TOKEN=
# File-based variant (secret-file pattern; wins only when the inline var is
# unset): path to a file whose trimmed content is the token.
# OMNIROUTE_INTERNAL_SERVICE_TOKEN_FILE=
# Quota Sharing (Group B — planos 16+22)
QUOTA_STORE_DRIVER=sqlite # sqlite | redis
# QUOTA_STORE_REDIS_URL= # ex.: redis://localhost:6379 (apenas quando driver=redis)
@@ -2370,3 +2479,38 @@ QUOTA_STORE_DRIVER=sqlite # sqlite | redis
# OMNIROUTE_DATA_DIR are both unset. Locates the Notion web-thread session cache.
# ─────────────────────────────────────────────────────────────────────────────
# VIBEPROXY_DATA_DIR=
# ── Internal service auth (management-plane service-to-service calls) ─────────
# Inline token for internal service authentication; prefer the _FILE variant in
# containerized deployments so the secret never lands in the environment table.
# OMNIROUTE_INTERNAL_SERVICE_TOKEN=
# Path to a file containing the internal service token (overrides the inline var).
# OMNIROUTE_INTERNAL_SERVICE_TOKEN_FILE=
# ═══════════════════════════════════════════════════════════════════════════════
# 26. RADAR FEED (SELF-HOSTING)
# ═══════════════════════════════════════════════════════════════════════════════
# Optional add-on (feature flag RADAR_ENABLED, default off — see feature flag
# settings, not an env var) that overlays a signed, freshly-curated free-model
# catalog on top of the release baseline. All four variables below are optional
# and only needed to point the client at a self-hosted/forked feed or
# supporter-key flow instead of the default OmniRoute Radar service. Used by:
# src/lib/radar/sync.ts, src/lib/radar/pinnedKeys.ts, src/lib/radar/links.ts.
# Base URL of the Radar feed service. Overrides the built-in default so forks
# and self-hosters can point at their own signed feed.
# RADAR_FEED_URL=https://radar.omniroute.online
# Ed25519 public key (base64-DER SPKI or PEM) used to verify the feed
# signature, replacing the pinned default key. Required when self-hosting a
# feed signed with a different key pair.
# RADAR_FEED_PUBKEY=
# URL the dashboard's "I'm a contributor" button opens (GitHub OAuth
# supporter-key claim flow). No pricing/value lives in this repo — only the
# link.
# RADAR_CONTRIBUTOR_CLAIM_URL=https://radar.omniroute.online/auth/github
# URL the dashboard's "Support the project" button opens (payment/plans
# page). No pricing/value lives in this repo — only the link.
# RADAR_SUPPORTER_PLANS_URL=https://radar.omniroute.online/planos

1
.eslintcache-probe Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1213,8 +1213,10 @@ jobs:
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
# (tsx/esm = QW-b; o alinhamento de ESCOPO do integration com o npm script fica p/ follow-up)
- run: node --import tsx/esm --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=1 --test-shard=${{ matrix.shard }}/2 tests/integration/*.test.ts
- name: Integration tests (shard ${{ matrix.shard }}/2)
env:
TEST_SHARD: ${{ matrix.shard }}/2
run: npm run test:integration:ci
test-security:
name: Security Tests

View File

@@ -22,10 +22,10 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
- uses: github/codeql-action/init@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4
with:
languages: javascript-typescript
queries: security-extended
- uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
- uses: github/codeql-action/analyze@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4
with:
category: "/language:javascript-typescript"

View File

@@ -4,6 +4,7 @@ on:
push:
branches:
- main
- "release/v*"
tags:
- "v*"
paths-ignore:
@@ -57,39 +58,20 @@ jobs:
REF_TYPE: ${{ github.ref_type }}
INPUT_VERSION: ${{ inputs.version }}
PROMOTE_INPUT: ${{ inputs.promote_latest }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
run: |
set -euo pipefail
# 1) Resolve version string from the trigger (all inputs come via env).
case "$EVENT_NAME" in
workflow_dispatch)
VERSION="${INPUT_VERSION#v}"
;;
push)
if [ "$REF_TYPE" = "tag" ]; then
VERSION="${REF_NAME#v}"
else
# Push to main → build & tag as `main` only. Never touch :latest.
VERSION="main"
fi
;;
release)
VERSION="${REF_NAME#v}"
;;
*)
VERSION="${REF_NAME#v}"
;;
esac
# Sanity-check: only allow [A-Za-z0-9._-] in VERSION (defense in depth).
if ! printf '%s' "$VERSION" | grep -qE '^[A-Za-z0-9._-]+$'; then
echo "Refusing to use unsafe VERSION value: $VERSION" >&2
exit 1
fi
# 1) Resolve version/channel from the trigger. Only the current default
# release branch publishes the mutable `next` channel; main keeps `main`.
VERSION=$(bash scripts/ci/resolve-docker-publish-version.sh \
"$EVENT_NAME" "$REF_TYPE" "$REF_NAME" "$INPUT_VERSION" "$DEFAULT_BRANCH")
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
# 2) Decide whether to promote :latest.
# 2) Decide whether to promote :latest. Floating channels are never
# eligible, and the helper independently fails closed for non-semver.
PROMOTE="false"
if [ "$VERSION" = "main" ]; then
if [ "$VERSION" = "main" ] || [ "$VERSION" = "next" ]; then
PROMOTE="false"
elif printf '%s' "$VERSION" | grep -qE -- '-(rc|alpha|beta|pre|next)'; then
echo "Pre-release identifier detected — skipping :latest."
@@ -109,10 +91,10 @@ jobs:
fi
echo "promote_latest=$PROMOTE" >> "$GITHUB_OUTPUT"
# 3) Skip if this exact version is already published in Docker Hub.
# `main` is always rebuilt (mutable floating tag).
# 3) Skip immutable version tags that already exist. Floating `main`
# and `next` channels are intentionally rebuilt on every matching push.
SKIP="false"
if [ "$VERSION" != "main" ]; then
if [ "$VERSION" != "main" ] && [ "$VERSION" != "next" ]; then
if docker manifest inspect "diegosouzapw/omniroute:${VERSION}" >/dev/null 2>&1; then
echo "Image diegosouzapw/omniroute:${VERSION} already exists on Docker Hub — skipping rebuild."
SKIP="true"
@@ -155,13 +137,13 @@ jobs:
uses: docker/setup-buildx-action@v4
- name: Login to Docker Hub
uses: docker/login-action@v4.5.2
uses: docker/login-action@v4.6.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v4.5.2
uses: docker/login-action@v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -255,13 +237,13 @@ jobs:
uses: docker/setup-buildx-action@v4
- name: Login to Docker Hub
uses: docker/login-action@v4.5.2
uses: docker/login-action@v4.6.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v4.5.2
uses: docker/login-action@v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -390,14 +372,14 @@ jobs:
- name: Upload Trivy SARIF to Security tab
if: needs.prepare.outputs.version != 'main'
continue-on-error: true
uses: github/codeql-action/upload-sarif@v4.37.3
uses: github/codeql-action/upload-sarif@v4.37.4
with:
sarif_file: trivy-results.sarif
category: trivy-image
- name: Update Docker Hub description
# Only refresh README/description when we actually promote :latest
# (avoids overwriting from main pushes or back-fill builds).
# (avoids overwriting from main, next, or back-fill builds).
if: needs.prepare.outputs.promote_latest == 'true'
uses: peter-evans/dockerhub-description@v5
with:

View File

@@ -271,6 +271,10 @@ jobs:
# covered by typecheck:core's curated allowlist. See check-dashboard-typecheck.mjs.
- name: Typecheck (dashboard)
run: npm run check:dashboard-typecheck
# #8781: open-sse workspace typecheck gate — the workspace imports @/ which
# escapes to src/ via undeclared path aliases. See check-open-sse-typecheck.mjs.
- name: Typecheck (open-sse)
run: npm run check:open-sse-typecheck
# WS4.2 (v3.8.49 plan): TypeScript 7 native-compiler SHADOW — advisory only.
# TS7 went GA 2026-07-08 with 8-12x type-check speedups; its Compiler API only
# arrives in 7.1, so typescript-eslint / type-coverage / Stryker stay on 6.x

5
.gitignore vendored
View File

@@ -72,6 +72,7 @@ yarn-error.log*
# env files (can opt-in for committing if needed)
.env*
!.env.example
!.env.devin-bridge.example
!.env.homolog.example
# Provider API keys (never commit)
*.api-key
@@ -209,6 +210,8 @@ scripts/i18n/_pending-keys.json
.agents/
.antigravitycli/
.claude/
!tests/fixtures/devin-bridge/e2e-workspace/.claude/
!tests/fixtures/devin-bridge/e2e-workspace/.claude/**
# PR Reviews and local feedback files
pr_reviews*.json
@@ -248,6 +251,8 @@ _artifacts/
# CI/local quality artifacts (eslint-results.json, quality-ratchet.md, etc.)
.artifacts/
# Isolated Devin bridge workspaces, evidence, and test databases
.sandbox/
# Homologation E2E suite (npm run homolog) — real-environment credentials + report output
.env.homolog

View File

@@ -30,7 +30,7 @@ omniroute setup opencode --auth
# 3. Restart OpenCode — /models lists the full live catalog
```
The `--auth` flag runs `opencode auth login --provider omniroute` automatically.
The `--auth` flag runs `opencode auth login --provider opencode-omniroute` automatically.
Use `--base-url` to point at a non-default OmniRoute address:
```sh
@@ -84,7 +84,7 @@ Peer dep: `@opencode-ai/plugin` (managed by your OpenCode install).
```
```sh
opencode auth login --provider omniroute
opencode auth login --provider opencode-omniroute
# prompts for the OmniRoute API key, writes to ~/.local/share/opencode/auth.json
```
@@ -164,8 +164,8 @@ Then in `~/.config/opencode/opencode.json` reference each directory by absolute
Paths are relative to `~/.config/opencode/`. Each entry now resolves to a distinct module file, so OC loads them as two separate plugin instances. Authenticate each:
```sh
opencode auth login --provider omniroute
opencode auth login --provider omniroute-preprod
opencode auth login --provider opencode-omniroute
opencode auth login --provider opencode-omniroute-preprod
```
Each entry gets its own provider id, its own model picker entry, its own slot in `auth.json`, and its own TTL cache. Closures are isolated per plugin instance — no cross-talk.
@@ -196,6 +196,7 @@ npm install --prefix ~/.config/opencode/plugins/omniroute-opencode-plugin-prepro
| Compression pipeline tags | Combo names get tagged with their compression pipeline (e.g. `Combo: claude-primary [rtk🟡 → caveman🟠]`) when `features.compressionMetadata: true`. Intensity tokens render as a traffic-light emoji: 🟢 lite/minimal · 🟡 standard · 🟠 aggressive/full · 🔴 ultra | both hooks |
| Provider-tag prefix | Prepend short upstream-provider label to enriched names (e.g. `Claude - Claude Opus 4.7` vs `Kiro - Claude Opus 4.7`, `GHM - GPT 5`) so same-id models routed via different upstream connections group visibly in the picker (default-on, opt-out via `features.providerTag: false`) | both hooks |
| Usable-only filter | Filter to providers with at least one healthy connection in `/api/providers` (opt-in via `features.usableOnly`) | both hooks |
| Model allowlist/blocklist | Curate the model picker to a fixed set of IDs via `features.visibleModels` (allowlist) and/or `features.hiddenModels` (blocklist). Bare suffixes like `claude-opus-4-7` match any `{prefix}/claude-opus-4-7`. Both compose with `usableOnly` (all filters AND together). Blocklist wins over allowlist (deny takes precedence) | both hooks |
| Disk-cache fallback | Last-known-good catalog persisted to disk; hydrates on a cold start when `/v1/models` is unreachable (default-on, opt-out via `features.diskCache: false`) | `config` |
| Bearer injection + suffix-spoof guard | Adds `Authorization` on baseURL-matched requests only | `auth.loader.fetch` |
| Gemini schema sanitization | Strips `$schema`/`$ref`/`additionalProperties` for `gemini-*`/`google-vertex-gemini/*` | `auth.loader.fetch` wrap |
@@ -226,6 +227,8 @@ Every field is optional. Defaults mirror v0.1.0 behaviour so existing `opencode.
| `compressionMetadata` | `boolean` | `false` | Pull `/api/context/combos` so combo names get tagged with their compression pipeline, e.g. `Combo: claude-primary [rtk🟡 → caveman🟠]`. Intensity tokens render as traffic-light emoji (🟢 lite/minimal · 🟡 standard · 🟠 aggressive/full · 🔴 ultra) so the picker advertises "how compressed" each combo is at a glance. |
| `providerTag` | `boolean` | `true` | Prepend a short upstream-provider label to the enriched display name with `" - "` separator, so `cc/claude-opus-4-7 → Claude - Claude Opus 4.7` differs visibly from `kr/claude-opus-4-7 → Kiro - Claude Opus 4.7` in the OC TUI model picker. Label resolution: use `/api/pricing/models[<alias>].name` verbatim when ≤8 chars (e.g. `Claude`, `Kiro`, `Codex`, `Qwen`), otherwise fall back to `UPPER(alias)` (e.g. `GitHub Models``GHM`, `Gemini``GEMINI`). Idempotent. Combos intentionally skipped (the `Combo:` prefix already conveys multi-upstream). |
| `usableOnly` | `boolean` | `false` | Read `/api/providers` and filter the catalog to providers that have at least one connection with `isActive: true` AND `testStatus: 'active'`. Subtract-filter semantics: providers unknown to BOTH the pricing-models catalog AND the connection table pass through (so synthetic prefixes like `agentrouter/*` survive). On fetch failure the filter is disabled for the refresh — never hides the whole catalog. |
| `visibleModels` | `string[]` | _unset_ | Allowlist — when set and non-empty, only models whose raw `/v1/models` ID matches are emitted. Bare IDs (no slash, e.g. `claude-opus-4-7`) match any `{prefix}/claude-opus-4-7`; full IDs (e.g. `cc/claude-opus-4-7`) match exactly. Composes with `usableOnly` and `hiddenModels` (all filters AND together). Unset or empty = no filter. |
| `hiddenModels` | `string[]` | _unset_ | Blocklist — models whose raw ID matches are dropped. Same matching rules as `visibleModels`. When a model is in both `visibleModels` and `hiddenModels`, the blocklist wins (deny takes precedence). Composes with `usableOnly` and `visibleModels` (all filters AND together). Unset or empty = no filter. |
| `diskCache` | `boolean` | `true` | Persist the last successful `/v1/models` + `/api/combos` + enrichment + connections + compression snapshot to `${OPENCODE_DATA_DIR ?? ~/.local/share/opencode}/plugins/omniroute-<providerId>.json`. On a subsequent cold start where `/v1/models` throws (network down / IP whitelist drop / 5xx) the static block hydrates from the snapshot so OC's model picker survives offline. Soft-fail on read/write — never blocks publishing. |
| `geminiSanitization` | `boolean` | `true` | Strip `$schema`/`$ref`/`additionalProperties` from tool params when the model id matches `gemini` |
| `mcpAutoEmit` | `boolean` | `false` | Auto-write an `mcp.<providerId>` remote entry into the OC config pointing at `<baseURL>/api/mcp/stream` with the resolved Bearer token |
@@ -298,7 +301,45 @@ If you want a narrower-scoped Bearer for MCP (different from the chat/inference
- `compressionMetadata: true` annotates combo display names with their pipeline using traffic-light emoji for intensity (e.g. `Combo: claude-primary [rtk🟡 → caveman🟠]`) so the picker advertises which compression each combo applies and how heavy it is at a glance. Palette: 🟢 lite/minimal · 🟡 standard · 🟠 aggressive/full · 🔴 ultra. Unknown intensities fall through to raw text (`[rtk:custom-thing]`) so the plugin never hides a value OmniRoute knows but the plugin doesn't.
- `providerTag: true` (default) prepends a short upstream-provider label so the picker shows `Claude - Claude Opus 4.7` for `cc/claude-opus-4-7`, `Kiro - Claude Opus 4.7` for `kr/claude-opus-4-7`, and `GHM - GPT 5` for `ghm/gpt-5` (slot.name `GitHub Models` > 8 chars → abbreviated). Critical when the same model id is sold through multiple upstream connections with different cost/auth/rate-limit profiles. Set to `false` to keep the pre-v3.8.3 unsuffixed format.
## Comparison vs `@omniroute/opencode-provider`
#### Example — curating the model picker (allowlist + blocklist)
A typical OmniRoute instance serves 600+ models. The OpenCode TUI/CLI picker becomes unusable when you need to scroll through hundreds of entries to find the ~30 models you actually use. `visibleModels` and `hiddenModels` let you curate the picker to a fixed set of model IDs that persists in `opencode.json` across config resets.
```jsonc
{
"plugin": [
[
"@omniroute/opencode-plugin",
{
"providerId": "omniroute",
"baseURL": "https://or.example.com",
"features": {
"combos": true,
"enrichment": true,
"usableOnly": true,
"visibleModels": [
"claude-opus-4-7", // bare suffix: matches cc/claude-opus-4-7, kr/claude-opus-4-7, etc.
"cc/claude-sonnet-4-6", // exact: only the cc/ alias
"gemini-2.5-pro",
"gpt-5",
"o3",
"o3-pro",
"o4-mini",
],
"hiddenModels": [
"o3-mini", // hide the mini variant even if visibleModels is unset
],
},
},
],
],
}
```
- `visibleModels` is an allowlist — only models whose raw ID matches are emitted. Bare IDs (no slash) match any provider prefix; full IDs (with slash) match exactly.
- `hiddenModels` is a blocklist — listed models are dropped. When a model is in both lists, the blocklist wins (deny takes precedence).
- Both compose with `usableOnly` (all filters AND together: a model must pass usableOnly AND visibleModels AND not be in hiddenModels).
- Unset or empty = no filter (current behavior).
[`@omniroute/opencode-provider`](https://github.com/diegosouzapw/OmniRoute/tree/main/%40omniroute/opencode-provider) is the existing config-generator package — it writes a frozen `provider.<id>` block into `opencode.json` at build time. This plugin is the runtime integration.

View File

@@ -1,12 +1,12 @@
{
"name": "@omniroute/opencode-plugin",
"version": "0.2.0",
"version": "0.2.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@omniroute/opencode-plugin",
"version": "0.2.0",
"version": "0.2.1",
"license": "MIT",
"dependencies": {
"zod": "^4.4.3"

View File

@@ -23,7 +23,7 @@
"scripts": {
"build": "tsup",
"clean": "rm -rf dist",
"test": "node --import tsx/esm --test tests/scaffold.test.ts tests/auth.test.ts tests/options-schema.test.ts tests/multi-instance.test.ts tests/fetch-interceptor.test.ts tests/provider.test.ts tests/gemini-sanitize.test.ts tests/combos.test.ts tests/config-shim.test.ts tests/features.test.ts tests/feature-defaults.test.ts tests/usable-combo.test.ts tests/disk-snapshot-perms.test.ts tests/fork-features.test.ts tests/auto-combo-context.test.ts tests/provider-id-routing.test.ts tests/management-read-token.test.ts tests/auto-sync.test.ts",
"test": "node --import tsx/esm --test tests/scaffold.test.ts tests/auth.test.ts tests/options-schema.test.ts tests/multi-instance.test.ts tests/fetch-interceptor.test.ts tests/provider.test.ts tests/gemini-sanitize.test.ts tests/combos.test.ts tests/config-shim.test.ts tests/features.test.ts tests/feature-defaults.test.ts tests/usable-combo.test.ts tests/disk-snapshot-perms.test.ts tests/fork-features.test.ts tests/auto-combo-context.test.ts tests/provider-id-routing.test.ts tests/management-read-token.test.ts tests/auto-sync.test.ts tests/model-allowlist.test.ts",
"prepublishOnly": "npm run clean && npm run build && npm test"
},
"keywords": [

View File

@@ -177,6 +177,8 @@ const featuresSchema = z
mcpToken: z.string().min(1).optional(),
fetchInterceptor: z.boolean().optional(),
usableOnly: z.boolean().optional(),
visibleModels: z.array(z.string().min(1)).optional(),
hiddenModels: z.array(z.string().min(1)).optional(),
diskCache: z.boolean().optional(),
providerTag: z.boolean().optional(),
debugLog: z.boolean().optional(),
@@ -241,6 +243,11 @@ export const OMNIROUTE_FEATURE_DEFAULTS = {
// default-OFF (read sites use `features.X === true`)
compressionMetadata: false,
usableOnly: false,
// Array flags: unset/empty = no filter. These are not boolean toggles —
// they are operator-curated model-ID lists applied in the dynamic and static
// hooks alongside usableOnly (all filters AND together).
// visibleModels: undefined, // allowlist — only listed IDs pass
// hiddenModels: undefined, // blocklist — listed IDs are dropped
mcpAutoEmit: false,
debugLog: false,
startupDebug: false,
@@ -330,7 +337,10 @@ function trimLeadingDashes(value: string): string {
* sees a consistent identifier.
*/
export function resolveOmniRoutePluginOptions(opts?: OmniRoutePluginOptions): Required<
Pick<OmniRoutePluginOptions, "providerId" | "displayName" | "modelCacheTtl" | "autoSyncIntervalMs">
Pick<
OmniRoutePluginOptions,
"providerId" | "displayName" | "modelCacheTtl" | "autoSyncIntervalMs"
>
> & {
/**
* #6859: the UNPREFIXED provider id ("omniroute", "omniroute-preprod", …).
@@ -621,7 +631,7 @@ export function createOmniRouteAuthHook(opts?: OmniRoutePluginOptions): AuthHook
*/
export function invalidateOmniRouteFetchCache(
cache: OmniRouteFetchCache,
baseURL?: string,
baseURL?: string
): number {
if (!baseURL) {
const n = cache.size;
@@ -645,7 +655,7 @@ export function invalidateOmniRouteFetchCache(
*/
export async function resolveOmniRouteRuntimeAuth(
resolved: ResolvedOmniRoutePluginOptions,
readAuthJson?: OmniRouteReadAuthJson,
readAuthJson?: OmniRouteReadAuthJson
): Promise<{ apiKey: string; baseURL: string; managementReadToken: string } | null> {
const reader = readAuthJson ?? defaultReadAuthJson;
let authJson: AuthJsonShape | undefined | null;
@@ -672,7 +682,7 @@ export async function resolveOmniRouteRuntimeAuth(
e &&
(e as { type?: unknown }).type === "api" &&
typeof (e as { key?: unknown }).key === "string" &&
((e as { key: string }).key).length > 0
(e as { key: string }).key.length > 0
) {
entry = e as AuthJsonApiEntry;
break;
@@ -737,7 +747,7 @@ export async function forceSyncOmniRouteModels(args: {
const auth = await resolveOmniRouteRuntimeAuth(
resolved,
args.readAuthJson ?? defaultReadAuthJson,
args.readAuthJson ?? defaultReadAuthJson
);
if (!auth) {
return {
@@ -795,7 +805,7 @@ export async function forceSyncOmniRouteModels(args: {
rawCompressionCombos = await compressionMetaFetcher(
auth.baseURL,
auth.managementReadToken,
10_000,
10_000
);
} catch {
rawCompressionCombos = [];
@@ -820,10 +830,7 @@ export async function forceSyncOmniRouteModels(args: {
rawConnections,
expiresAt: t + resolved.modelCacheTtl,
};
const cacheKey = modelsCacheKey(
auth.baseURL,
`${auth.apiKey}\0${auth.managementReadToken}`,
);
const cacheKey = modelsCacheKey(auth.baseURL, `${auth.apiKey}\0${auth.managementReadToken}`);
cache.set(cacheKey, entry);
if (wantDiskCache) {
@@ -831,7 +838,7 @@ export async function forceSyncOmniRouteModels(args: {
const fingerprint = diskSnapshotIdentityFingerprint(
auth.baseURL,
auth.apiKey,
auth.managementReadToken,
auth.managementReadToken
);
const { expiresAt: _expiresAt, ...diskEntry } = entry;
await defaultDiskSnapshotWriter(resolved.providerId, diskEntry, fingerprint);
@@ -843,7 +850,7 @@ export async function forceSyncOmniRouteModels(args: {
console.warn(
`[omniroute-plugin] force sync ok providerId=${resolved.providerId} ` +
`models=${rawModels.length} combos=${rawCombos.length} ` +
`clearedMemory=${clearedMemory + clearedAll} disk=${clearedDisk}`,
`clearedMemory=${clearedMemory + clearedAll} disk=${clearedDisk}`
);
return {
@@ -944,7 +951,7 @@ export function startOmniRouteAutoSync(args: {
const result = await forceSyncOmniRouteModels({ resolved, cache });
if (!result.ok) {
console.warn(
`[omniroute-plugin] auto-sync failed providerId=${resolved.providerId}: ${result.error}`,
`[omniroute-plugin] auto-sync failed providerId=${resolved.providerId}: ${result.error}`
);
return;
}
@@ -955,7 +962,7 @@ export function startOmniRouteAutoSync(args: {
if (result.count !== lastCount) {
console.warn(
`[omniroute-plugin] auto-sync catalog size changed ${lastCount}${result.count} ` +
`(providerId=${resolved.providerId})`,
`(providerId=${resolved.providerId})`
);
lastCount = result.count;
}
@@ -976,7 +983,7 @@ export function startOmniRouteAutoSync(args: {
}
console.warn(
`[omniroute-plugin] auto-sync enabled intervalMs=${intervalMs} providerId=${resolved.providerId}`,
`[omniroute-plugin] auto-sync enabled intervalMs=${intervalMs} providerId=${resolved.providerId}`
);
return () => {
@@ -1032,7 +1039,13 @@ export const OmniRoutePlugin: Plugin = async (_input, options) => {
const cfg = input as Config & {
command?: Record<
string,
{ template: string; description?: string; agent?: string; model?: string; subtask?: boolean }
{
template: string;
description?: string;
agent?: string;
model?: string;
subtask?: boolean;
}
>;
};
if (!cfg.command) cfg.command = {};
@@ -2820,6 +2833,118 @@ export function isUsableCombo(
return false;
}
// ─────────────────────────────────────────────────────────────────────────
// #9473 — Model allowlist / blocklist filter helpers
// ─────────────────────────────────────────────────────────────────────────
/**
* Pre-compiled filter structure for the model allowlist/blocklist.
*
* "exact" holds full raw IDs (e.g. "cc/claude-opus-4-7") for O(1) match.
* "suffixes" holds bare model IDs (e.g. "claude-opus-4-7") that match any
* "{prefix}/claude-opus-4-7" — so operators can curate by model name without
* knowing the provider prefix.
*/
export interface ModelListFilter {
exact: Set<string>;
suffixes: Set<string>;
}
/**
* Compile a string[] of model IDs into a pre-computed filter structure.
* Returns undefined when the list is empty or undefined — the "no filter"
* state that callers use as a passthrough.
*
* IDs containing a "/" are stored in "exact"; bare IDs (no slash) go into
* "suffixes" and match any "{prefix}/<suffix>" at check time.
*/
export function compileModelListFilter(list?: string[]): ModelListFilter | undefined {
if (!list || list.length === 0) return undefined;
const exact = new Set<string>();
const suffixes = new Set<string>();
for (const id of list) {
if (id.includes("/")) {
exact.add(id);
} else {
suffixes.add(id);
}
}
if (exact.size === 0 && suffixes.size === 0) return undefined;
return { exact, suffixes };
}
/**
* Decide whether a raw model ID passes the allowlist/blocklist filter.
*
* Rules (all filters AND together with usableOnly):
* - No visible filter and no hidden filter → keep (passthrough).
* - Visible filter set: id must match either the exact set or the suffix
* set (bare suffix "claude-opus-4-7" matches any "{prefix}/claude-opus-4-7").
* - Hidden filter set: id must NOT match either the exact or suffix set.
* - If id is in BOTH visible and hidden → DROP (deny wins — safer).
* - No-slash ids (e.g. combo names like "claude-primary") are checked
* against the exact set directly, and against the suffix set as a bare
* match.
*
* Pure function — exported so static + dynamic hooks share the same
* verdict logic without divergence.
*/
export function passesModelAllowlist(
id: string,
visible?: ModelListFilter,
hidden?: ModelListFilter
): boolean {
// Hidden filter takes precedence (deny wins over allow).
if (hidden) {
if (hidden.exact.has(id) || matchesSuffix(id, hidden.suffixes)) return false;
}
// Visible filter: if set, id must match.
if (visible) {
if (!visible.exact.has(id) && !matchesSuffix(id, visible.suffixes)) return false;
}
return true;
}
/**
* Decide whether a combo passes the allowlist filter. A combo keeps when
* AT LEAST ONE of its members matches the visible filter. When no visible
* filter is set, all combos pass. Combos with zero resolvable members pass
* (mirrors `isUsableCombo` semantics).
*/
export function passesComboAllowlist(
combo: OmniRouteRawCombo,
visible?: ModelListFilter
): boolean {
if (!visible) return true;
const steps = Array.isArray(combo.models) ? combo.models : [];
if (steps.length === 0) return true;
let sawResolvableMember = false;
for (const step of steps) {
if (step?.kind === "combo-ref") continue;
const modelId = typeof step?.model === "string" ? step.model : "";
if (modelId.length === 0) continue;
sawResolvableMember = true;
if (visible.exact.has(modelId) || matchesSuffix(modelId, visible.suffixes)) return true;
}
// No resolvable member → can't prove it should be hidden; keep.
if (!sawResolvableMember) return true;
// Every resolvable member failed the allowlist → drop.
return false;
}
/**
* Check whether a raw model ID matches any suffix in the set.
* For an id like `cc/claude-opus-4-7`, the suffix after the first `/`
* is checked against the suffixes set. For a bare id like `claude-primary`,
* the id itself is checked against the suffixes set.
*/
function matchesSuffix(id: string, suffixes: Set<string>): boolean {
if (suffixes.size === 0) return false;
const slash = id.indexOf("/");
const suffix = slash > 0 ? id.slice(slash + 1) : id;
return suffixes.has(suffix);
}
/**
* Slugify a combo display name into a copy/paste-friendly URL-safe segment.
* Lowercases, replaces any run of non-alphanumeric chars with a single dash,
@@ -3003,6 +3128,9 @@ export function createOmniRouteProviderHook(
const wantCompressionMeta = features.compressionMetadata === true;
const wantUsableOnly = features.usableOnly === true;
const wantProviderTag = features.providerTag !== false;
// #9473: model allowlist/blocklist — compile once per hook instance.
const visibleFilter = compileModelListFilter(features.visibleModels);
const hiddenFilter = compileModelListFilter(features.hiddenModels);
const now = deps.now ?? Date.now;
// T-07: cache holds RAW fetch results (not pre-derived ModelV2) so that
// the config-shim hook can share the same cache and derive its stripped
@@ -3237,6 +3365,8 @@ export function createOmniRouteProviderHook(
if (!entry.id) continue;
if (canonicalDedup.has(entry.id)) continue;
if (usable && !isUsableRawModelId(entry.id, usable, rawEnrichment)) continue;
// #9473: allowlist/blocklist filter (AND with usableOnly).
if (!passesModelAllowlist(entry.id, visibleFilter, hiddenFilter)) continue;
const model = mapRawModelToModelV2(entry, {
// #6859: server-facing id — NOT the OC-gate-prefixed `resolved.providerId`.
providerId: resolved.omnirouteProviderId,
@@ -3312,6 +3442,8 @@ export function createOmniRouteProviderHook(
if (!combo.id) return false;
if (combo.isHidden === true) return false;
if (usable && !isUsableCombo(combo, usable)) return false;
// #9473: combo allowlist — drop when no member matches visible filter.
if (visibleFilter && !passesComboAllowlist(combo, visibleFilter)) return false;
return true;
});
// Resolved nested combos keyed by their friendly name, so parent
@@ -4129,6 +4261,9 @@ export function buildStaticProviderEntry(
wantUsableOnly && connections && connections.length > 0
? usableProviderAliasSet(connections, enrichment)
: undefined;
// #9473: model allowlist/blocklist — compile once per static-block build.
const visibleFilter = compileModelListFilter(opts.features?.visibleModels);
const hiddenFilter = compileModelListFilter(opts.features?.hiddenModels);
// Provider-tag suffix — default-on, opt-out via `features.providerTag: false`.
// Prepends e.g. `Claude - ` to enriched raw-model names so the picker
// can tell `cc/claude-opus-4-7` (Anthropic) apart from `kr/claude-opus-4-7`
@@ -4166,6 +4301,8 @@ export function buildStaticProviderEntry(
// Skip canonical-named twins when the alias-keyed enriched row exists.
if (canonicalDedup.has(raw.id)) continue;
if (usable && !isUsableRawModelId(raw.id, usable, enrichment)) continue;
// #9473: allowlist/blocklist filter (AND with usableOnly).
if (!passesModelAllowlist(raw.id, visibleFilter, hiddenFilter)) continue;
const caps = raw.capabilities ?? {};
// Enrichment overlay: `/api/pricing/models` carries human display names
// (e.g. "Claude Opus 4.7" for raw id "cc/claude-opus-4-7"). The OC TUI
@@ -4266,11 +4403,11 @@ export function buildStaticProviderEntry(
entry.release_date = raw.release_date;
}
// OC's static-catalog reader parses each key on `/` and rejects the
// entire provider block if ANY key resolves to a parsed providerID that
// has no corresponding provider block. So bare keys (no `/`) MUST be
// prefixed with the resolved providerId. Already-prefixed keys
// (e.g. `cc/claude-opus-4-7`) are left as-is to avoid double-prefixing.
// #9175: OC's `getModel` looks the model up by BARE id — the part after
// the first `/` in the user's request — so a dict key with an embedded
// provider prefix (`<providerId>/<raw-id>`) is unreachable. Keys are the
// raw id verbatim; ids that already contain `/` (e.g. `cc/claude-opus-4-7`)
// keep it because the slash is part of the upstream model id itself.
models[raw.id] = entry;
}
@@ -4318,6 +4455,8 @@ export function buildStaticProviderEntry(
if (!combo.id) return false;
if (combo.isHidden === true) return false;
if (usable && !isUsableCombo(combo, usable)) return false;
// #9473: combo allowlist — drop when no member matches visible filter.
if (visibleFilter && !passesComboAllowlist(combo, visibleFilter)) return false;
return true;
});
@@ -4466,7 +4605,8 @@ export function buildStaticProviderEntry(
// (`opencode-omniroute/opencode-omniroute/<slug>`), and `parseModel()`
// resolves credentials for the nonexistent provider `opencode-omniroute`
// instead of `omniroute`. See #7976.
models[buildComboKey(combo, usedComboKeys, opts.omnirouteProviderId).split("/").pop()!] = entry;
models[buildComboKey(combo, usedComboKeys, opts.omnirouteProviderId).split("/").pop()!] =
entry;
// Make this combo's resolved entry available to parent combos
// that reference it via combo-ref. Use the friendly name since

View File

@@ -227,7 +227,7 @@ test("config: with valid auth.json + apiKey + baseURL → mutates input.provider
// Stripped per-model shape: name + cap flags + modalities + (optional)
// cost. OC's SDK static schema accepts only `limit.{context,output}` —
// `limit.input` is NOT in the SDK shape and gets dropped silently.
const claude = entry.models["opencode-omniroute/claude-sonnet-4-6"];
const claude = entry.models["claude-sonnet-4-6"];
assert.ok(claude, "claude model surfaced");
assert.equal(claude.name, "claude-sonnet-4-6");
assert.equal(claude.attachment, true);
@@ -248,7 +248,7 @@ test("config: with valid auth.json + apiKey + baseURL → mutates input.provider
// Combo surfaces under bare key + LCD'd
// (gemini's reasoning=false → combo reasoning=false).
const combo = entry.models["omniroute/claude-tier"];
const combo = entry.models["claude-tier"];
assert.ok(combo, "combo surfaced under bare key");
assert.equal(combo.name, "Claude Tier");
assert.equal(combo.reasoning, false, "LCD: any member reasoning=false → combo reasoning=false");
@@ -471,10 +471,10 @@ test("config: combos fetcher throws → emit models-only catalog (no combos in m
assert.ok(entry);
const ids = Object.keys(entry.models).sort();
assert.deepEqual(ids, [
"opencode-omniroute/claude-sonnet-4-6",
"opencode-omniroute/gemini-3-flash",
"claude-sonnet-4-6",
"gemini-3-flash",
]);
assert.equal(entry.models["omniroute/claude-tier"], undefined, "no combo entry");
assert.equal(entry.models["claude-tier"], undefined, "no combo entry");
assert.ok(
logger.entries.some((e) => String(e[0]).includes("/api/combos fetch failed")),
"combos-fetch breadcrumb emitted"
@@ -723,7 +723,7 @@ test("buildStaticProviderEntry: stripped per-model shape matches sibling @omniro
}
// Sanity: claude entry has all expected stripped fields.
const claude = block.models["opencode-omniroute/claude-sonnet-4-6"];
const claude = block.models["claude-sonnet-4-6"];
assert.equal(typeof claude.name, "string");
assert.equal(typeof claude.attachment, "boolean");
assert.equal(typeof claude.reasoning, "boolean");
@@ -748,8 +748,8 @@ test("buildStaticProviderEntry: hidden combos are excluded", () => {
"https://or.example/v1",
"sk-test"
);
assert.equal(block.models["omniroute/claude-tier"], undefined);
assert.ok(block.models["opencode-omniroute/claude-sonnet-4-6"]);
assert.equal(block.models["claude-tier"], undefined);
assert.ok(block.models["claude-sonnet-4-6"]);
});
// ────────────────────────────────────────────────────────────────────────────
@@ -765,7 +765,7 @@ test("buildStaticProviderEntry: emits modalities.input from raw.input_modalities
"https://or.example/v1",
"sk-test"
);
const claude = block.models["opencode-omniroute/claude-sonnet-4-6"];
const claude = block.models["claude-sonnet-4-6"];
assert.deepEqual(claude.modalities?.input, ["text", "image"]);
assert.deepEqual(claude.modalities?.output, ["text"]);
});
@@ -779,7 +779,7 @@ test("buildStaticProviderEntry: never emits limit.input (OC SDK rejects it)", ()
"https://or.example/v1",
"sk-test"
);
const claude = block.models["opencode-omniroute/claude-sonnet-4-6"];
const claude = block.models["claude-sonnet-4-6"];
assert.equal((claude.limit as Record<string, unknown>).input, undefined);
assert.equal(typeof claude.limit?.context, "number");
assert.equal(typeof claude.limit?.output, "number");
@@ -807,7 +807,7 @@ test("buildStaticProviderEntry: emits cost when enrichment carries pricing", ()
"sk-test",
enrichment
);
const claude = block.models["opencode-omniroute/claude-sonnet-4-6"];
const claude = block.models["claude-sonnet-4-6"];
assert.equal(claude.cost?.input, 3);
assert.equal(claude.cost?.output, 15);
assert.equal(claude.cost?.cache_read, 0.3);
@@ -828,8 +828,8 @@ test("buildStaticProviderEntry: emits release_date when raw carries it; omits wh
"https://or.example/v1",
"sk-test"
);
assert.equal(block.models["opencode-omniroute/claude-with-date"].release_date, "2026-02-19");
assert.equal(block.models["opencode-omniroute/gemini-3-flash"].release_date, undefined);
assert.equal(block.models["claude-with-date"].release_date, "2026-02-19");
assert.equal(block.models["gemini-3-flash"].release_date, undefined);
});
test("buildStaticProviderEntry: combo modalities = intersection of members (LCD)", () => {
@@ -858,7 +858,7 @@ test("buildStaticProviderEntry: combo modalities = intersection of members (LCD)
"https://or.example/v1",
"sk-test"
);
const combo = block.models["omniroute/mixed-tier"];
const combo = block.models["mixed-tier"];
assert.ok(combo, "combo emitted under slug key");
// claude has text+image, text-only has text → intersection drops image.
assert.deepEqual(combo.modalities?.input, ["text"]);
@@ -967,10 +967,10 @@ test("config: enrichment fetched + name overlaid on raw-model entries", async ()
"opencode-omniroute"
];
assert.ok(entry);
assert.equal(entry.models["opencode-omniroute/claude-sonnet-4-6"].name, "Claude Sonnet 4.6");
assert.equal(entry.models["opencode-omniroute/gemini-3-flash"].name, "Gemini 3 Flash");
assert.equal(entry.models["claude-sonnet-4-6"].name, "Claude Sonnet 4.6");
assert.equal(entry.models["gemini-3-flash"].name, "Gemini 3 Flash");
// Combo names still come from /api/combos — enrichment overlay does NOT touch combos.
assert.equal(entry.models["omniroute/claude-tier"].name, "Claude Tier");
assert.equal(entry.models["claude-tier"].name, "Claude Tier");
assert.equal(enrichmentFetcher.callCount(), 1);
});
@@ -1000,7 +1000,7 @@ test("config: features.enrichment=false skips enrichment fetch + keeps raw-id na
assert.ok(entry);
assert.equal(enrichmentFetcher.callCount(), 0, "enrichment fetch suppressed by feature flag");
assert.equal(
entry.models["opencode-omniroute/claude-sonnet-4-6"].name,
entry.models["claude-sonnet-4-6"].name,
"claude-sonnet-4-6",
"raw id retained"
);
@@ -1027,7 +1027,7 @@ test("config: enrichment fetcher throws → soft-fail (warn + raw-id static cata
];
assert.ok(entry, "static block still published on enrichment failure");
assert.equal(
entry.models["opencode-omniroute/claude-sonnet-4-6"].name,
entry.models["claude-sonnet-4-6"].name,
"claude-sonnet-4-6",
"raw id retained"
);
@@ -1229,11 +1229,11 @@ test("config: diskCache hydrates stale snapshot when /v1/models throws", async (
"opencode-omniroute"
];
assert.ok(
entry.models["opencode-omniroute/claude-sonnet-4-6"],
entry.models["claude-sonnet-4-6"],
"stale snapshot hydrated into static block"
);
assert.equal(
entry.models["opencode-omniroute/claude-sonnet-4-6"].name,
entry.models["claude-sonnet-4-6"].name,
"Claude Sonnet 4.6 (cached)",
"stale enrichment also reused"
);
@@ -1281,7 +1281,7 @@ test("config: cached rawEnrichment from earlier provider hook is reused (no refe
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
assert.equal(entry.models["opencode-omniroute/claude-sonnet-4-6"].name, "Claude Sonnet 4.6");
assert.equal(entry.models["claude-sonnet-4-6"].name, "Claude Sonnet 4.6");
});
// ─────────────────────────────────────────────────────────────────────
@@ -1332,12 +1332,12 @@ test("config: providerTag (default-on) prepends '<provider> - ' to enriched raw-
];
assert.ok(entry);
assert.equal(
entry.models["opencode-omniroute/claude-sonnet-4-6"].name,
entry.models["claude-sonnet-4-6"].name,
"Claude - Claude Sonnet 4.6"
);
assert.equal(entry.models["opencode-omniroute/gemini-3-flash"].name, "Gemini - Gemini 3 Flash");
assert.equal(entry.models["gemini-3-flash"].name, "Gemini - Gemini 3 Flash");
// Combos stay untouched — `Combo: ` prefix already conveys multi-upstream.
assert.equal(entry.models["omniroute/claude-tier"].name, "Claude Tier");
assert.equal(entry.models["claude-tier"].name, "Claude Tier");
});
test("config: providerTag=false suppresses the suffix", async () => {
@@ -1364,7 +1364,7 @@ test("config: providerTag=false suppresses the suffix", async () => {
"opencode-omniroute"
];
assert.equal(
entry.models["opencode-omniroute/claude-sonnet-4-6"].name,
entry.models["claude-sonnet-4-6"].name,
"Claude Sonnet 4.6",
"enriched name kept, provider tag suppressed"
);
@@ -1396,7 +1396,7 @@ test("config: providerTag falls back to UPPER(alias) when providerDisplayName mi
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
assert.equal(entry.models["opencode-omniroute/claude-sonnet-4-6"].name, "CC - Claude Sonnet 4.6");
assert.equal(entry.models["claude-sonnet-4-6"].name, "CC - Claude Sonnet 4.6");
});
test("config: providerTag skipped entirely when neither providerDisplayName nor providerAlias set", async () => {
@@ -1423,7 +1423,7 @@ test("config: providerTag skipped entirely when neither providerDisplayName nor
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
assert.equal(entry.models["opencode-omniroute/claude-sonnet-4-6"].name, "Claude Sonnet 4.6");
assert.equal(entry.models["claude-sonnet-4-6"].name, "Claude Sonnet 4.6");
});
test("config: providerTag is idempotent — second hook call doesn't double-suffix", async () => {
@@ -1451,7 +1451,7 @@ test("config: providerTag is idempotent — second hook call doesn't double-suff
"opencode-omniroute"
];
assert.equal(
entryA.models["opencode-omniroute/claude-sonnet-4-6"].name,
entryA.models["claude-sonnet-4-6"].name,
"Claude - Claude Sonnet 4.6"
);
@@ -1462,7 +1462,7 @@ test("config: providerTag is idempotent — second hook call doesn't double-suff
"opencode-omniroute"
];
assert.equal(
entryB.models["opencode-omniroute/claude-sonnet-4-6"].name,
entryB.models["claude-sonnet-4-6"].name,
"Claude - Claude Sonnet 4.6"
);
});
@@ -1516,7 +1516,7 @@ test("buildStaticProviderEntry: nested combo-ref context is the bottleneck acros
);
// Pre-fix: Parent would advertise 200_000 (only raw-big counted).
// Post-fix: Parent should advertise 8_000 (TinyCombo bottleneck).
const parent = block.models["omniroute/parent"];
const parent = block.models["parent"];
assert.ok(parent, "Parent combo must be in the static catalog");
assert.equal(parent.limit?.context, 8_000);
});

View File

@@ -0,0 +1,317 @@
/**
* #9473 — Model allowlist/blocklist for the opencode-plugin.
*
* Tests for the pure filter helpers (`compileModelListFilter`,
* `passesModelAllowlist`, `passesComboAllowlist`) and the schema + hook-level
* integration. The allowlist/blocklist composes with `usableOnly` (all filters
* AND together), blocklist wins over allowlist (deny takes precedence), and
* bare-suffix entries (e.g. "claude-opus-4-7") match any "{prefix}/claude-opus-4-7".
*/
import test from "node:test";
import assert from "node:assert/strict";
import {
compileModelListFilter,
passesModelAllowlist,
passesComboAllowlist,
parseOmniRoutePluginOptions,
buildStaticProviderEntry,
resolveOmniRoutePluginOptions,
type OmniRouteRawCombo,
type OmniRouteRawModelEntry,
} from "../src/index.js";
// ─────────────────────────────────────────────────────────────────────────
// compileModelListFilter
// ─────────────────────────────────────────────────────────────────────────
test("compileModelListFilter: undefined list → undefined", () => {
assert.equal(compileModelListFilter(undefined), undefined);
});
test("compileModelListFilter: empty array → undefined", () => {
assert.equal(compileModelListFilter([]), undefined);
});
test("compileModelListFilter: raw IDs with slash → exact set populated", () => {
const f = compileModelListFilter(["cc/claude-opus-4-7", "glm/gpt-5"]);
assert.ok(f);
assert.equal(f.exact.has("cc/claude-opus-4-7"), true);
assert.equal(f.exact.has("glm/gpt-5"), true);
assert.equal(f.suffixes.size, 0);
});
test("compileModelListFilter: bare IDs (no slash) → suffixes set populated", () => {
const f = compileModelListFilter(["claude-opus-4-7", "gpt-5"]);
assert.ok(f);
assert.equal(f.suffixes.has("claude-opus-4-7"), true);
assert.equal(f.suffixes.has("gpt-5"), true);
assert.equal(f.exact.size, 0);
});
test("compileModelListFilter: mixed raw + bare → both sets populated", () => {
const f = compileModelListFilter(["cc/claude-opus-4-7", "gpt-5"]);
assert.ok(f);
assert.equal(f.exact.has("cc/claude-opus-4-7"), true);
assert.equal(f.suffixes.has("gpt-5"), true);
});
// ─────────────────────────────────────────────────────────────────────────
// passesModelAllowlist
// ─────────────────────────────────────────────────────────────────────────
test("passesModelAllowlist: no visible, no hidden → keep (passthrough)", () => {
assert.equal(passesModelAllowlist("cc/claude-opus-4-7", undefined, undefined), true);
});
test("passesModelAllowlist: visible undefined, hidden undefined → keep", () => {
assert.equal(passesModelAllowlist("cc/claude-opus-4-7", undefined, undefined), true);
});
test("passesModelAllowlist: visible set, id matches exact → keep", () => {
const vis = compileModelListFilter(["cc/claude-opus-4-7"]);
assert.equal(passesModelAllowlist("cc/claude-opus-4-7", vis, undefined), true);
});
test("passesModelAllowlist: visible set, id matches suffix → keep", () => {
const vis = compileModelListFilter(["claude-opus-4-7"]);
assert.equal(passesModelAllowlist("cc/claude-opus-4-7", vis, undefined), true);
});
test("passesModelAllowlist: visible set, id does NOT match → drop", () => {
const vis = compileModelListFilter(["cc/claude-opus-4-7"]);
assert.equal(passesModelAllowlist("glm/gpt-5", vis, undefined), false);
});
test("passesModelAllowlist: visible set, bare suffix matches different prefix → keep", () => {
const vis = compileModelListFilter(["claude-opus-4-7"]);
assert.equal(passesModelAllowlist("kr/claude-opus-4-7", vis, undefined), true);
});
test("passesModelAllowlist: hidden set, id matches exact → drop", () => {
const hid = compileModelListFilter(["cc/claude-opus-4-7"]);
assert.equal(passesModelAllowlist("cc/claude-opus-4-7", undefined, hid), false);
});
test("passesModelAllowlist: hidden set, id matches suffix → drop", () => {
const hid = compileModelListFilter(["claude-opus-4-7"]);
assert.equal(passesModelAllowlist("cc/claude-opus-4-7", undefined, hid), false);
});
test("passesModelAllowlist: hidden set, id does NOT match → keep", () => {
const hid = compileModelListFilter(["cc/claude-opus-4-7"]);
assert.equal(passesModelAllowlist("glm/gpt-5", undefined, hid), true);
});
test("passesModelAllowlist: id in BOTH visible and hidden → DROP (deny wins)", () => {
const vis = compileModelListFilter(["cc/claude-opus-4-7"]);
const hid = compileModelListFilter(["cc/claude-opus-4-7"]);
assert.equal(passesModelAllowlist("cc/claude-opus-4-7", vis, hid), false);
});
test("passesModelAllowlist: visible allows, hidden blocks different id → keep the visible one", () => {
const vis = compileModelListFilter(["cc/claude-opus-4-7"]);
const hid = compileModelListFilter(["glm/gpt-5"]);
assert.equal(passesModelAllowlist("cc/claude-opus-4-7", vis, hid), true);
assert.equal(passesModelAllowlist("glm/gpt-5", vis, hid), false);
});
test("passesModelAllowlist: bare-suffix hidden blocks exact match too", () => {
const hid = compileModelListFilter(["claude-opus-4-7"]);
assert.equal(passesModelAllowlist("cc/claude-opus-4-7", undefined, hid), false);
assert.equal(passesModelAllowlist("kr/claude-opus-4-7", undefined, hid), false);
});
test("passesModelAllowlist: no-slash id, visible set has bare match → keep", () => {
const vis = compileModelListFilter(["claude-primary"]);
assert.equal(passesModelAllowlist("claude-primary", vis, undefined), true);
});
test("passesModelAllowlist: no-slash id, visible set has no match → drop", () => {
const vis = compileModelListFilter(["cc/claude-opus-4-7"]);
assert.equal(passesModelAllowlist("claude-primary", vis, undefined), false);
});
// ─────────────────────────────────────────────────────────────────────────
// passesComboAllowlist
// ─────────────────────────────────────────────────────────────────────────
function combo(models: OmniRouteRawCombo["models"]): OmniRouteRawCombo {
return { id: "c1", name: "Test Combo", models };
}
test("passesComboAllowlist: visible undefined → keep", () => {
const c = combo([{ kind: "model", model: "cc/claude-opus-4-7" }]);
assert.equal(passesComboAllowlist(c, undefined), true);
});
test("passesComboAllowlist: ≥1 member matches visible → keep", () => {
const vis = compileModelListFilter(["cc/claude-opus-4-7"]);
const c = combo([
{ kind: "model", model: "dead/legacy" },
{ kind: "model", model: "cc/claude-opus-4-7" },
]);
assert.equal(passesComboAllowlist(c, vis), true);
});
test("passesComboAllowlist: zero members match visible → drop", () => {
const vis = compileModelListFilter(["cc/claude-opus-4-7"]);
const c = combo([
{ kind: "model", model: "glm/gpt-5" },
{ kind: "model", model: "kr/claude-opus-4-7" },
]);
assert.equal(passesComboAllowlist(c, vis), false);
});
test("passesComboAllowlist: bare suffix matches any prefix → keep", () => {
const vis = compileModelListFilter(["claude-opus-4-7"]);
const c = combo([{ kind: "model", model: "kr/claude-opus-4-7" }]);
assert.equal(passesComboAllowlist(c, vis), true);
});
test("passesComboAllowlist: zero members → keep", () => {
const vis = compileModelListFilter(["cc/claude-opus-4-7"]);
assert.equal(passesComboAllowlist(combo([]), vis), true);
assert.equal(passesComboAllowlist(combo(undefined), vis), true);
});
test("passesComboAllowlist: only combo-ref steps → keep", () => {
const vis = compileModelListFilter(["cc/claude-opus-4-7"]);
const c = combo([{ kind: "combo-ref", comboName: "nested" }]);
assert.equal(passesComboAllowlist(c, vis), true);
});
// ─────────────────────────────────────────────────────────────────────────
// Schema — visibleModels / hiddenModels
// ─────────────────────────────────────────────────────────────────────────
test("parseOmniRoutePluginOptions: visibleModels string[] → preserved", () => {
const r = parseOmniRoutePluginOptions({
features: { visibleModels: ["cc/claude-opus-4-7", "gpt-5"] },
});
assert.deepEqual(r.features?.visibleModels, ["cc/claude-opus-4-7", "gpt-5"]);
});
test("parseOmniRoutePluginOptions: hiddenModels string[] → preserved", () => {
const r = parseOmniRoutePluginOptions({
features: { hiddenModels: ["glm/gpt-5"] },
});
assert.deepEqual(r.features?.hiddenModels, ["glm/gpt-5"]);
});
test("parseOmniRoutePluginOptions: both lists together → preserved", () => {
const r = parseOmniRoutePluginOptions({
features: {
visibleModels: ["cc/claude-opus-4-7"],
hiddenModels: ["glm/gpt-5"],
},
});
assert.deepEqual(r.features?.visibleModels, ["cc/claude-opus-4-7"]);
assert.deepEqual(r.features?.hiddenModels, ["glm/gpt-5"]);
});
test("parseOmniRoutePluginOptions: empty string in visibleModels → rejects", () => {
assert.throws(
() =>
parseOmniRoutePluginOptions({
features: { visibleModels: [""] },
}),
/Invalid @omniroute\/opencode-plugin options/
);
});
test("parseOmniRoutePluginOptions: empty string in hiddenModels → rejects", () => {
assert.throws(
() =>
parseOmniRoutePluginOptions({
features: { hiddenModels: [""] },
}),
/Invalid @omniroute\/opencode-plugin options/
);
});
test("parseOmniRoutePluginOptions: unknown features key still rejects (strict invariant)", () => {
assert.throws(
() =>
parseOmniRoutePluginOptions({
features: { visibleModels: ["x"], unknownKey: true },
}),
/Invalid @omniroute\/opencode-plugin options/
);
});
// ─────────────────────────────────────────────────────────────────────────
// buildStaticProviderEntry — allowlist/blocklist integration
// ─────────────────────────────────────────────────────────────────────────
const FAKE_RAW_MODELS: OmniRouteRawModelEntry[] = [
{ id: "cc/claude-opus-4-7", owned_by: "anthropic" },
{ id: "glm/gpt-5", owned_by: "openai" },
{ id: "kr/claude-opus-4-7", owned_by: "anthropic" },
{ id: "claude-primary", owned_by: "combo" },
];
test("buildStaticProviderEntry: no allowlist → all models emitted", () => {
const opts = resolveOmniRoutePluginOptions({ features: {} });
const entry = buildStaticProviderEntry(FAKE_RAW_MODELS, [], opts, "http://localhost:20128/v1", "sk-test");
const ids = Object.keys(entry.models);
assert.ok(ids.includes("cc/claude-opus-4-7"), "cc/claude-opus-4-7 should be present");
assert.ok(ids.includes("glm/gpt-5"), "glm/gpt-5 should be present");
assert.ok(ids.includes("kr/claude-opus-4-7"), "kr/claude-opus-4-7 should be present");
});
test("buildStaticProviderEntry: visibleModels filters to only listed IDs", () => {
const opts = resolveOmniRoutePluginOptions({
features: { visibleModels: ["cc/claude-opus-4-7"] },
});
const entry = buildStaticProviderEntry(FAKE_RAW_MODELS, [], opts, "http://localhost:20128/v1", "sk-test");
const ids = Object.keys(entry.models);
assert.ok(ids.includes("cc/claude-opus-4-7"), "cc/claude-opus-4-7 should be present");
assert.equal(ids.includes("glm/gpt-5"), false, "glm/gpt-5 should be filtered out");
assert.equal(ids.includes("kr/claude-opus-4-7"), false, "kr/claude-opus-4-7 should be filtered out");
});
test("buildStaticProviderEntry: hiddenModels drops listed IDs", () => {
const opts = resolveOmniRoutePluginOptions({
features: { hiddenModels: ["glm/gpt-5"] },
});
const entry = buildStaticProviderEntry(FAKE_RAW_MODELS, [], opts, "http://localhost:20128/v1", "sk-test");
const ids = Object.keys(entry.models);
assert.ok(ids.includes("cc/claude-opus-4-7"), "cc/claude-opus-4-7 should be present");
assert.equal(ids.includes("glm/gpt-5"), false, "glm/gpt-5 should be hidden");
assert.ok(ids.includes("kr/claude-opus-4-7"), "kr/claude-opus-4-7 should be present");
});
test("buildStaticProviderEntry: bare-suffix visibleModels matches any prefix", () => {
const opts = resolveOmniRoutePluginOptions({
features: { visibleModels: ["claude-opus-4-7"] },
});
const entry = buildStaticProviderEntry(FAKE_RAW_MODELS, [], opts, "http://localhost:20128/v1", "sk-test");
const ids = Object.keys(entry.models);
assert.ok(ids.includes("cc/claude-opus-4-7"), "cc/claude-opus-4-7 should match via suffix");
assert.ok(ids.includes("kr/claude-opus-4-7"), "kr/claude-opus-4-7 should match via suffix");
assert.equal(ids.includes("glm/gpt-5"), false, "glm/gpt-5 should be filtered out");
});
test("buildStaticProviderEntry: id in both visible and hidden → hidden wins", () => {
const opts = resolveOmniRoutePluginOptions({
features: {
visibleModels: ["cc/claude-opus-4-7"],
hiddenModels: ["cc/claude-opus-4-7"],
},
});
const entry = buildStaticProviderEntry(FAKE_RAW_MODELS, [], opts, "http://localhost:20128/v1", "sk-test");
const ids = Object.keys(entry.models);
assert.equal(ids.includes("cc/claude-opus-4-7"), false, "deny takes precedence");
});
test("buildStaticProviderEntry: empty visibleModels → no filter (passthrough)", () => {
const opts = resolveOmniRoutePluginOptions({
features: { visibleModels: [] },
});
const entry = buildStaticProviderEntry(FAKE_RAW_MODELS, [], opts, "http://localhost:20128/v1", "sk-test");
const ids = Object.keys(entry.models);
assert.ok(ids.includes("cc/claude-opus-4-7"), "empty visibleModels should not filter");
assert.ok(ids.includes("glm/gpt-5"), "empty visibleModels should not filter");
});

View File

@@ -111,7 +111,9 @@ test("#6859: createOmniRouteProviderHook end-to-end — catalog keys/providerID
// `opencode-omniroute`. Confirmed against the issue's own curl repro
// (`model: "opencode-omniroute/hermes-smart-stack"` → "No active
// credentials for provider: opencode-omniroute").
test("#7976: buildStaticProviderEntry keys bare-slug combo ids with the unprefixed omnirouteProviderId (no double OC-gate prefix)", () => {
// #9175 tightened this further: OC's `getModel` looks models up by BARE id,
// so combo dict keys now carry NO prefix at all (not even `omniroute/`).
test("#7976/#9175: buildStaticProviderEntry keys combos by bare slug (no prefix at all — never the OC-gate providerId)", () => {
const resolved = resolveOmniRoutePluginOptions({ providerId: "omniroute" });
assert.equal(resolved.providerId, "opencode-omniroute");
assert.equal(resolved.omnirouteProviderId, "omniroute");
@@ -131,7 +133,7 @@ test("#7976: buildStaticProviderEntry keys bare-slug combo ids with the unprefix
"sk-test"
);
assert.deepEqual(Object.keys(block.models), ["omniroute/hermes-smart-stack"]);
assert.deepEqual(Object.keys(block.models), ["hermes-smart-stack"]);
assert.equal(
block.models["opencode-omniroute/hermes-smart-stack"],
undefined,

View File

@@ -58,7 +58,7 @@ Repository map and Reference Documentation sections below.
| Services | `open-sse/services/` | Combo routing, rate limits, caching, etc |
| Database | `src/lib/db/` | SQLite domain modules (130 migrations) |
| Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic |
| MCP Server | `open-sse/mcp-server/` | 104 tools (42 base + memory/skill/agentSkill/pool/notion/obsidian/gamification/plugin modules), 3 transports (stdio / SSE / Streamable HTTP), 31 scopes |
| MCP Server | `open-sse/mcp-server/` | 105 tools (42 base + memory/skill/agentSkill/pool/notion/obsidian/gamification/plugin modules), 3 transports (stdio / SSE / Streamable HTTP), 31 scopes |
| A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol |
| Skills | `src/lib/skills/` | Extensible skill framework |
| Memory | `src/lib/memory/` | Persistent conversational memory |
@@ -399,6 +399,7 @@ For any non-trivial change, read the matching deep-dive first:
| Resilience (3 mechanisms) | `docs/architecture/RESILIENCE_GUIDE.md` |
| Reasoning replay | `docs/routing/REASONING_REPLAY.md` |
| Skills framework | `docs/frameworks/SKILLS.md` |
| Radar (free-model catalog overlay) | `docs/frameworks/RADAR.md` |
| Memory system (FTS5 + Qdrant) | `docs/frameworks/MEMORY.md` |
| Cloud agents | `docs/frameworks/CLOUD_AGENT.md` |
| Guardrails (PII / injection / vision) | `docs/security/GUARDRAILS.md` |
@@ -427,7 +428,7 @@ For any non-trivial change, read the matching deep-dive first:
| What | Command |
| ----------------------- | --------------------------------------------------------------------------- |
| Unit tests | `npm run test:unit` |
| Single file | `node --import tsx/esm --test tests/unit/your-file.test.ts` |
| Single file | `node --import tsx/esm --test tests/unit/your-file.test.ts` |
| Vitest (MCP, autoCombo) | `npm run test:vitest` |
| E2E (Playwright) | `npm run test:e2e` |
| Protocol E2E (MCP+A2A) | `npm run test:protocols:e2e` |

View File

@@ -15,6 +15,12 @@ coverage, and reconciliation steps.
- **Node.js** `>=22.22.3 <23`, or `>=24.0.0 <27` (recommended: 24 LTS)
- **npm** 10+
> **npm v11+ users (Node 24+):** After `npm install`, verify native modules were installed:
> `node -e "require('better-sqlite3')"`. If it fails with `MODULE_NOT_FOUND`,
> run `npm approve-scripts better-sqlite3 && npm install`. See
> [Troubleshooting](docs/guides/TROUBLESHOOTING.md#npm-v11-better-sqlite3-not-installed-cannot-find-module).
- **Git**
### Clone & Install

View File

@@ -77,7 +77,7 @@ RUN test -f package-lock.json \
# a broken/rate-limited fetch fails the BUILD loudly instead of shipping a
# broken image.
RUN --mount=type=cache,id=npm-cache,target=/root/.npm \
npm ci --no-audit --no-fund --legacy-peer-deps --ignore-scripts \
npm ci --include=optional --no-audit --no-fund --legacy-peer-deps --ignore-scripts \
&& (cd node_modules/better-sqlite3 \
&& node /usr/local/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js rebuild) \
&& node -e "require('better-sqlite3')(':memory:').close()" \
@@ -119,7 +119,9 @@ ENV NODE_OPTIONS="--max-old-space-size=${OMNIROUTE_BUILD_MEMORY_MB}"
COPY . ./
RUN --mount=type=cache,id=next-cache,target=/app/.build/next/cache \
mkdir -p /app/data && npm run build
mkdir -p /app/data \
&& npm run build \
&& node --input-type=module -e "import { createRequire } from 'node:module'; import { pathToFileURL } from 'node:url'; const standaloneRoot = '/app/.build/next/standalone/node_modules/'; const require = createRequire('/app/.build/next/standalone/package.json'); for (const pkg of ['@atjsh/llmlingua-2', '@huggingface/transformers', '@tensorflow/tfjs', 'js-tiktoken']) { const resolved = require.resolve(pkg); if (!resolved.startsWith(standaloneRoot)) throw new Error(pkg + ' resolved outside standalone: ' + resolved); await import(pathToFileURL(resolved).href); } const onnxRuntime = require.resolve('onnxruntime-node'); if (!onnxRuntime.startsWith(standaloneRoot)) throw new Error('onnxruntime-node resolved outside standalone: ' + onnxRuntime); await import(pathToFileURL(onnxRuntime).href);"
# ── Runner base ────────────────────────────────────────────────────────────
FROM base AS runner-base

0
MAX Normal file
View File

View File

@@ -188,7 +188,7 @@ curl http://localhost:20128/v1/chat/completions \
</div>
<img src="./docs/diagrams/promise-pillars.svg" width="100%" alt="The Promise — One endpoint. 291 providers. Never stop building — OmniRoute picks the cheapest one that works. Six pillars: Never hit limits (auto-fallback across 291 providers in milliseconds, zero downtime) · Save up to 95% tokens (RTK + Caveman stacked compression cuts 1595%, ~89% avg on tool-heavy sessions) · $0 to start (90+ free tiers, 40+ free forever — no card needed) · Every tool works (33 coding agents through one config) · One endpoint (OpenAI ↔ Claude ↔ Gemini ↔ Responses API at /v1) · Production-grade (circuit breakers, TLS stealth, MCP 104 tools, A2A, memory, guardrails, evals — 25,000+ tests)."/>
<img src="./docs/diagrams/promise-pillars.svg" width="100%" alt="The Promise — One endpoint. 291 providers. Never stop building — OmniRoute picks the cheapest one that works. Six pillars: Never hit limits (auto-fallback across 291 providers in milliseconds, zero downtime) · Save up to 95% tokens (RTK + Caveman stacked compression cuts 1595%, ~89% avg on tool-heavy sessions) · $0 to start (90+ free tiers, 40+ free forever — no card needed) · Every tool works (33 coding agents through one config) · One endpoint (OpenAI ↔ Claude ↔ Gemini ↔ Responses API at /v1) · Production-grade (circuit breakers, TLS stealth, MCP 105 tools, A2A, memory, guardrails, evals — 25,000+ tests)."/>
<br/>
<br/>
@@ -439,7 +439,7 @@ All **19** strategies — mix & match per combo step:
</div>
<img src="./docs/diagrams/comparison-table.svg" width="100%" alt="What sets OmniRoute apart — comparison table vs 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute: 291 providers, 90+ free providers built-in, 19 routing strategies, 12-engine token compression, built-in MCP server with 104 tools, A2A agent protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, Desktop/Termux/PWA, 43 i18n UI locales, 100% MIT self-hosted. OmniRoute is the only one with the full set; competitors show a mix of checks, partials and crosses. Verified from each project&apos;s docs."/>
<img src="./docs/diagrams/comparison-table.svg" width="100%" alt="What sets OmniRoute apart — comparison table vs 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute: 291 providers, 90+ free providers built-in, 19 routing strategies, 12-engine token compression, built-in MCP server with 105 tools, A2A agent protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, Desktop/Termux/PWA, 43 i18n UI locales, 100% MIT self-hosted. OmniRoute is the only one with the full set; competitors show a mix of checks, partials and crosses. Verified from each project&apos;s docs."/>
<sub>📊 Full methodology &amp; per-feature detail vs 9router, OpenRouter, CLIProxyAPI &amp; LiteLLM → [`docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md`](docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md)</sub>
@@ -723,7 +723,7 @@ Expose OmniRoute over **MCP**, **A2A**, a **REST API**, **webhooks** or a **remo
<table>
<tr><th align="left">Interface</th><th align="left">Endpoint / command</th><th align="left">Use it for</th></tr>
<tr><td align="left" nowrap>🧰 <b>MCP (stdio)</b></td><td align="left" nowrap><code>omniroute --mcp</code></td><td align="left">Plug into Claude Desktop, Cursor, any MCP client</td></tr>
<tr><td align="left" nowrap>🌊 <b>MCP (HTTP)</b></td><td align="left" nowrap><code>/api/mcp/stream</code></td><td align="left">Remote MCP — <b>104 tools</b>, 31 scopes, full audit trail</td></tr>
<tr><td align="left" nowrap>🌊 <b>MCP (HTTP)</b></td><td align="left" nowrap><code>/api/mcp/stream</code></td><td align="left">Remote MCP — <b>105 tools</b>, 31 scopes, full audit trail</td></tr>
<tr><td align="left" nowrap>📡 <b>MCP (SSE)</b></td><td align="left" nowrap><code>/api/mcp/sse</code></td><td align="left">Streaming MCP transport</td></tr>
<tr><td align="left" nowrap>🤝 <b>A2A</b></td><td align="left" nowrap><code>/.well-known/agent.json</code></td><td align="left">Agent-to-agent, <b>JSON-RPC 2.0</b> + SSE, 6 skills</td></tr>
<tr><td align="left" nowrap>🌐 <b>REST API</b></td><td align="left" nowrap><code>/v1/*</code></td><td align="left">OpenAI-compatible — chat, embeddings, images, audio, OCR</td></tr>
@@ -890,6 +890,12 @@ docker run -d --name omniroute --restart unless-stopped --stop-timeout 40 \
-p 127.0.0.1:20128:20128 -v omniroute-data:/app/data diegosouzapw/omniroute:latest
```
> **Pre-release Docker channel:** `diegosouzapw/omniroute:next` and
> `diegosouzapw/omniroute:next-web` follow the current default `release/v*`
> branch. These mutable tags are intended only for testing unreleased fixes and
> are **not supported for production**. See
> [Docker Release Channels](docs/guides/DOCKER_RELEASE_CHANNELS.md).
**🛠️ From source**
```bash

1
_tasks Symbolic link
View File

@@ -0,0 +1 @@
/home/diegosouzapw/dev/proxys/OmniRoute/_tasks

View File

@@ -299,7 +299,15 @@ async function checkNativeBinary(rootDir) {
"Release",
"better_sqlite3.node"
),
path.join(rootDir, "dist", "node_modules", "better-sqlite3", "build", "Release", "better_sqlite3.node"),
path.join(
rootDir,
"dist",
"node_modules",
"better-sqlite3",
"build",
"Release",
"better_sqlite3.node"
),
path.join(rootDir, "node_modules", "better-sqlite3", "build", "Release", "better_sqlite3.node"),
];
const binaryPath = candidates.find((candidate) => fs.existsSync(candidate));
@@ -396,7 +404,10 @@ async function checkServerLiveness(options = {}) {
// First attempt: configured health endpoint (may require auth token).
const primary = await probeUrl(url);
if (primary.ok) {
return ok("Server liveness", "Server health endpoint is reachable", { url, status: primary.status });
return ok("Server liveness", "Server health endpoint is reachable", {
url,
status: primary.status,
});
}
// #6162: /api/health and /api/health/degradation require a management token.
@@ -427,7 +438,12 @@ async function checkServerLiveness(options = {}) {
return ok(
"Server liveness",
`Server reachable (health endpoint returned ${primary.status}, likely requires MANAGEMENT_TOKEN)`,
{ primaryUrl: url, primaryStatus: primary.status, fallbackUrl, fallbackStatus: fallback.status }
{
primaryUrl: url,
primaryStatus: primary.status,
fallbackUrl,
fallbackStatus: fallback.status,
}
);
}
@@ -440,8 +456,7 @@ async function checkServerLiveness(options = {}) {
export async function collectDoctorChecks(context = {}, options = {}) {
const rootDir =
context.rootDir ||
path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
context.rootDir || path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
const dataDir = resolveDataDir();
const dbPath = resolveStoragePath(dataDir);

View File

@@ -159,9 +159,7 @@ async function runBrowserFlow(def, opts) {
}
const result = await exchangeRes.json();
const conn = result.connection ?? {};
process.stdout.write(
`Authorized: ${conn.email ?? conn.displayName ?? conn.id ?? "connected"}\n`
);
process.stdout.write(`Authorized: ${conn.email ?? conn.displayName ?? conn.id ?? "connected"}\n`);
}
async function safeErrorBody(res) {

View File

@@ -160,8 +160,7 @@ export async function runSetupClaudeCommand(opts = {}) {
let detail = `HTTP ${res.status}`;
try {
const errorBody = await res.json();
const serverMsg =
errorBody?.error?.message || errorBody?.error || errorBody?.message || "";
const serverMsg = errorBody?.error?.message || errorBody?.error || errorBody?.message || "";
if (serverMsg) detail += `${serverMsg}`;
} catch {}
throw new Error(detail);

View File

@@ -218,6 +218,26 @@ function registerPluginInOpenCodeConfig({
* a clear "could not run opencode" message instead of a hard import
* failure.
*/
/**
* Resolve the provider id used for `opencode auth login --provider <id>`.
*
* The bundled @omniroute/opencode-plugin registers its provider under
* `opencode-<id>` (the `opencode-` prefix is required by OpenCode >=1.17.8's
* native-adapter gate). The auth login command must use the prefixed form
* because OpenCode resolves `--provider <id>` against the provider id the
* plugin actually registered.
*
* Idempotent: if the id already starts with `opencode-`, it passes through
* unchanged. This protects users who manually worked around the bug with
* `--provider opencode-omniroute`.
*
* @param {string} providerId
* @returns {string}
*/
export function resolveOpenCodeAuthProviderId(providerId) {
return providerId.startsWith("opencode-") ? providerId : `opencode-${providerId}`;
}
/**
* Pure resolver for the `opencode auth login` spawn descriptor. Extracted so the
* platform-branching logic is unit-testable without mocking child_process or
@@ -231,21 +251,23 @@ function registerPluginInOpenCodeConfig({
*/
export function resolveOpenCodeAuthSpawn(providerId, platform = process.platform) {
const isWin = platform === "win32";
const authProviderId = resolveOpenCodeAuthProviderId(providerId);
return {
command: isWin ? "opencode.cmd" : "opencode",
args: ["auth", "login", "--provider", providerId],
args: ["auth", "login", "--provider", authProviderId],
options: { stdio: "inherit", shell: isWin },
};
}
export function runOpenCodeAuth(providerId) {
const authProviderId = resolveOpenCodeAuthProviderId(providerId);
const { command, args, options } = resolveOpenCodeAuthSpawn(providerId);
const res = spawnSync(command, args, options);
if (res.error) {
// ENOENT = opencode is not on PATH
if (res.error.code === "ENOENT") {
printInfo(
`opencode CLI not found on PATH. Run \`opencode auth login --provider ${providerId}\` manually after installing OpenCode.`
`opencode CLI not found on PATH. Run \`opencode auth login --provider ${authProviderId}\` manually after installing OpenCode.`
);
return 1;
}
@@ -343,7 +365,8 @@ export async function runSetupOpenCodeCommand(opts = {}) {
if (wantsAuth) {
if (nonInteractive) {
printInfo(`Skipping \`opencode auth login\` (non-interactive mode).`);
printInfo(`Run manually: opencode auth login --provider ${providerId}`);
const authProviderId = resolveOpenCodeAuthProviderId(providerId);
printInfo(`Run manually: opencode auth login --provider ${authProviderId}`);
} else {
printHeading("Authenticating with OpenCode");
const authExit = runOpenCodeAuth(providerId);
@@ -352,8 +375,9 @@ export async function runSetupOpenCodeCommand(opts = {}) {
}
}
} else {
const authProviderId = resolveOpenCodeAuthProviderId(providerId);
printInfo(
`Next step: opencode auth login --provider ${providerId} (pass --auth to do this automatically)`
`Next step: opencode auth login --provider ${authProviderId} (pass --auth to do this automatically)`
);
}

View File

@@ -188,16 +188,18 @@ export async function runUpdateCommand(opts = {}) {
const afterVersion = await getCurrentVersion();
if (afterVersion && compareVersions(afterVersion, latest) < 0) {
printError(
`Global install updated to ${latest}, but the running binary still reports ${afterVersion}.`,
`Global install updated to ${latest}, but the running binary still reports ${afterVersion}.`
);
console.log(
" A local `node_modules/omniroute` is likely shadowing the global install on PATH.",
" A local `node_modules/omniroute` is likely shadowing the global install on PATH."
);
console.log(" Diagnose with:");
console.log(" which -a omniroute");
console.log(" command -v omniroute");
console.log(" npm prefix -g");
console.log(" Then remove the shadowing local copy (e.g. `npm uninstall omniroute` from its directory)");
console.log(
" Then remove the shadowing local copy (e.g. `npm uninstall omniroute` from its directory)"
);
console.log(" or reorder PATH so the global bin comes first.");
return 1;
}

View File

@@ -94,10 +94,12 @@ export function isBetterSqliteBinaryValid() {
const magic = buf.toString("hex");
const os = platform();
let formatOk;
if (os === "linux") formatOk = magic.startsWith("7f454c46"); // ELF
if (os === "linux")
formatOk = magic.startsWith("7f454c46"); // ELF
else if (os === "darwin")
formatOk = magic.startsWith("cffaedfe") || magic.startsWith("cefaedfe"); // Mach-O
else if (os === "win32") formatOk = magic.startsWith("4d5a"); // PE/MZ
else if (os === "win32")
formatOk = magic.startsWith("4d5a"); // PE/MZ
else formatOk = true;
if (!formatOk) return false;
// File-format magic bytes alone do not guarantee the binary was built for the Node ABI

View File

@@ -17,7 +17,7 @@ export const SYSTRAY_VERSION = "2.1.4";
const SYSTRAY_SPEC = `${SYSTRAY_PACKAGE}@${SYSTRAY_VERSION}`;
export function resolveSystrayBinName(platform: NodeJS.Platform): string | null {
if (platform === "win32") return null;
if (platform === "win32") return "tray_windows_release.exe";
if (platform === "darwin") return "tray_darwin_release";
return "tray_linux_release";
}
@@ -45,7 +45,6 @@ export function chmodSystrayBinAt(runtimeRoot: string, platform: NodeJS.Platform
}
export async function loadSystray(): Promise<(new (...args: unknown[]) => unknown) | null> {
if (process.platform === "win32") return null; // Windows uses tray.ps1 instead
ensureRuntimeDir();
if (!isInstalled()) {
try {

View File

@@ -130,7 +130,7 @@ async function openSqliteDatabase(dbPath, options = {}) {
try {
return new loaded.Database(dbPath, options);
} catch (error) {
throw createSqliteNativeError(error);
return openWithSyncDriverFallback(dbPath, options, error);
}
}

View File

@@ -167,6 +167,10 @@ export function getAutostartStatus() {
linger: tryReadLingerEnabled(),
};
}
if (process.platform === "win32") {
const winMechanism = isAutostartEnabled() ? "vbs-startup" : null;
return { enabled: isAutostartEnabled(), mechanism: winMechanism };
}
return { enabled: isAutostartEnabled(), mechanism: null };
}

View File

@@ -1,5 +1,4 @@
import { isTraySupported, initSystrayUnix, killSystrayUnix } from "./traySystray.mjs";
import { initWinTray, killWinTray } from "./trayWindows.mjs";
let active = null;
@@ -10,15 +9,17 @@ export async function initTray({ port, onQuit, onOpenDashboard, onShowLogs }) {
const ctx = { port, onQuit, onOpenDashboard, onShowLogs };
// initSystrayUnix is async: it lazily installs/loads systray2 from the runtime
// dir (trayRuntime.ts) rather than from node_modules. (#4605)
active = process.platform === "win32" ? initWinTray(ctx) : await initSystrayUnix(ctx);
// Use systray2 on all platforms including Windows — the tarball ships
// tray_windows_release.exe, avoiding the Norton/AVG IDP.HELU.PSE85 heuristic
// that fires on temp-dir PowerShell scripts. (#8609)
active = await initSystrayUnix(ctx);
return active;
}
export function killTray() {
if (!active) return;
try {
if (process.platform === "win32") killWinTray(active);
else killSystrayUnix(active);
killSystrayUnix(active);
} catch {}
active = null;
}

View File

@@ -3,7 +3,7 @@
import { spawn } from "node:child_process";
import { existsSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { fileURLToPath, pathToFileURL } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -43,7 +43,15 @@ export async function startMcpCli(rootDir = ROOT) {
}
// `tsx` loader is only required for local `.ts` fallback; JS entry works without it.
const loaderArgs = mcpEntry.endsWith(".ts") ? ["--import", "tsx"] : [];
const tsxLoaderArgs = mcpEntry.endsWith(".ts") ? ["--import", "tsx"] : [];
// Preload the stdout/stderr console guard before mcpEntry's own module graph evaluates —
// DB init (a side effect of createMcpServer()'s tool registration) logs via plain
// console.log, and by the time any code inside mcpEntry itself could redirect it, that
// module's own (hoisted) imports have already run. Loading the guard first, in a separate
// module, is the only point early enough to guarantee it never leaks into the JSON-RPC
// stream on stdout.
const consoleGuard = pathToFileURL(join(__dirname, "mcpStdioConsoleGuard.mjs")).href;
const loaderArgs = ["--import", consoleGuard, ...tsxLoaderArgs];
await new Promise((resolve, reject) => {
const child = spawn(process.execPath, [...loaderArgs, mcpEntry], {

View File

@@ -0,0 +1,16 @@
// Preloaded (via `node --import`) before open-sse/mcp-server/server.ts and its entire
// import graph evaluate. The stdio MCP transport uses stdout exclusively for JSON-RPC
// messages, but DB init (getDbInstance(), triggered as a side effect of evaluating the
// server's module graph — e.g. tool registration reading compression settings) logs via
// plain console.log. A redirect placed *inside* server.ts (even at the top of its first
// executed function) is too late: static imports are hoisted and fully evaluated before
// any of that function's own code runs, so earlier console.log calls during import-time
// side effects already escaped to the real stdout by then. Redirecting here, in a module
// that loads before server.ts is even requested, is the only point early enough to
// guarantee no startup output leaks into the JSON-RPC stream and corrupts it client-side
// (e.g. Claude Desktop: "Unexpected token 'D', \"[DB] Changi\"... is not valid JSON").
import { Console } from "node:console";
const stderrConsole = new Console({ stdout: process.stderr, stderr: process.stderr });
console.log = stderrConsole.log.bind(stderrConsole);
console.warn = stderrConsole.warn.bind(stderrConsole);

View File

@@ -43,6 +43,19 @@ if (isVersionFastPath(process.argv)) {
process.exit(0);
}
// MCP stdio transport uses stdout exclusively for JSON-RPC messages. Redirect
// console.log/warn to stderr before anything else runs — including the tsx/esm and
// polyfill imports below, since those (and their transitive module graphs, e.g. DB
// init) can themselves log during evaluation. Redirecting after those imports let
// early output leak straight into the JSON-RPC stream and corrupt it client-side
// (e.g. Claude Desktop: "Unexpected token 'D', \"[DB] Changi\"... is not valid JSON").
if (process.argv.includes("--mcp")) {
const { Console } = await import("node:console");
const stderrConsole = new Console({ stdout: process.stderr, stderr: process.stderr });
console.log = stderrConsole.log.bind(stderrConsole);
console.warn = stderrConsole.warn.bind(stderrConsole);
}
// Register tsx so dynamic imports of .ts source files (referenced as .js per
// TypeScript conventions) resolve correctly. The build never emits .js for
// src/lib/cli-helper/, so tsx handles the .ts → .js resolution at runtime.
@@ -58,16 +71,6 @@ await import("../open-sse/utils/setupPolyfill.ts");
const { registerAliasResolver } = await import("./aliasResolver.mjs");
await registerAliasResolver(ROOT);
// MCP stdio transport uses stdout exclusively for JSON-RPC messages.
// Redirect console.log/warn to stderr early (before loadEnvFile and DB init)
// so no startup output corrupts the protocol.
if (process.argv.includes("--mcp")) {
const { Console } = await import("node:console");
const stderrConsole = new Console({ stdout: process.stderr, stderr: process.stderr });
console.log = stderrConsole.log.bind(stderrConsole);
console.warn = stderrConsole.warn.bind(stderrConsole);
}
// Electron persists secrets (JWT_SECRET, API_KEY_SECRET, STORAGE_ENCRYPTION_KEY) to
// `<DATA_DIR>/server.env` (electron/main.js), never `.env`. Migrating an existing
// install (storage.sqlite + server.env) to the CLI left those secrets undiscoverable —

View File

@@ -39,7 +39,8 @@ snap="$(ops_find_snapshot "$ID")"
# Policy definition tables present in BOTH the snapshot and the live DB. GLOB
# keeps `_` literal; we drop usage counters / logs so accounting isn't rewound.
readarray -t tables < <(
tables=()
while IFS= read -r t; do tables+=("$t"); done < <(
sqlite3 "$snap/storage.sqlite" \
"SELECT name FROM sqlite_master WHERE type='table' AND name GLOB 'api_key*' \
AND name NOT GLOB '*counter*' AND name NOT GLOB '*_log*' ORDER BY name;"

View File

@@ -0,0 +1 @@
- **feat(api):** add response content encoding verification — confirms Next.js compress:true and documents stripStaleForwardingHeaders behavior ([#6736](https://github.com/diegosouzapw/OmniRoute/issues/6736))

View File

@@ -0,0 +1 @@
- **feat(api):** add plugins marketplace install endpoint with checksum verification ([#6752](https://github.com/diegosouzapw/OmniRoute/issues/6752))

View File

@@ -0,0 +1 @@
- **feat(chatgpt-web):** harden prompt-emulated tool contract for thinking models (#7679 — thanks @horacecar)

View File

@@ -0,0 +1 @@
- **docs:** add low-memory/small VPS optimization guide ([#8237](https://github.com/diegosouzapw/OmniRoute/issues/8237))

View File

@@ -0,0 +1 @@
- **feat(providers):** add connection-level custom upstream headers via `provider_specific_data.customHeaders` — applied to every request through that connection, with model-level headers overriding on the same case-insensitive name. (thanks @Benson-mk)

View File

@@ -0,0 +1 @@
- feat(copilot): add approval gate for runOmniRouteCli commands (#8461)

View File

@@ -0,0 +1 @@
- **feat(electron):** Desktop app can now attach to an already-running OmniRoute server (e.g. a Docker/OrbStack container) instead of always spawning its own bundled server — configurable via the tray's "Remote Server → Connect to Remote Server…" or the `OMNIROUTE_REMOTE_URL` env var ([#8799](https://github.com/diegosouzapw/OmniRoute/pull/8799)) — thanks @soulhakr

View File

@@ -0,0 +1 @@
- **feat(providers):** add Regolo AI OpenAI-compatible provider ([#9031](https://github.com/diegosouzapw/OmniRoute/issues/9031))

View File

@@ -0,0 +1 @@
- **feat(db):** add provider-scoped model aliases that survive rediscovery ([#9068](https://github.com/diegosouzapw/OmniRoute/issues/9068))

View File

@@ -0,0 +1 @@
- feat: make forwarded upstream response-header budget configurable via env var (#9243)

View File

@@ -0,0 +1 @@
- **feat(providers):** make video_url passthrough configurable per provider/model via compat override ([#9248](https://github.com/diegosouzapw/OmniRoute/issues/9248)) — thanks @HellFiveOsborn

View File

@@ -0,0 +1 @@
- **feat(dashboard):** render a conditional "Get API key" link on the provider detail page, surfaced from the existing `notice.apiKeyUrl` / `notice.signupUrl` catalog metadata (e.g. `pioneer`, `jina`, `together`). The link opens in a new tab and is hidden when neither URL is present, so existing providers are unaffected. Tracks the notice field in `ProviderCatalogMetadata` ([#9270](https://github.com/diegosouzapw/OmniRoute/pull/9270))

View File

@@ -0,0 +1 @@
- **feat(providers):** accept JSON cookie objects in normalizeSessionCookieHeader (#9284 — thanks @AIB1TAL0S)

View File

@@ -0,0 +1 @@
- **feat(providers):** support max reasoning effort for opencode-zen DeepSeek models (#9318)

View File

@@ -0,0 +1 @@
- **sse:** New-API / One-API / Sub2API aggregator balance detection for compatible nodes — with the "Aggregator Gateway" toggle on, OmniRoute queries the aggregator's `/api/user/self` to read the account balance, shows it as a dashboard badge and lets quota-preflight routing skip exhausted accounts. Gated by the `NEWAPI_AGGREGATOR_BALANCE` feature flag (default off), with a `quotaPerUnit` override for aggregators that do not use the default 500000 units/$1 rate ([#9415](https://github.com/diegosouzapw/OmniRoute/issues/9415))

View File

@@ -0,0 +1 @@
- **feat(catalog):** added opt-in settings `hideAutoCombos` and `hideNoThinkVariants` (default off) to filter built-in `auto/*` virtual combos and `no-think/*` gateway variants from the `/v1/models` catalog — user-defined combos and original provider models stay listed; routing is unaffected ([#9418](https://github.com/diegosouzapw/OmniRoute/issues/9418))

View File

@@ -0,0 +1 @@
- **feat(opencode-plugin):** added `features.visibleModels` (allowlist) and `features.hiddenModels` (blocklist) to `@omniroute/opencode-plugin` — curate the OpenCode TUI/CLI model picker from 600+ catalog entries down to an operator-defined ID list that persists in `opencode.json` across config resets ([#9473](https://github.com/diegosouzapw/OmniRoute/issues/9473))

View File

@@ -0,0 +1 @@
- **feat(models):** Test All's "Auto-hide failed models" no longer hides quota errors — daily-quota-exhausted and credits-exhausted responses are now classified via the routing path's existing quota detectors, so an evening Test All on a free-tier provider no longer silently wipes the catalog. Quota results stay visible with a distinct amber badge ([#9511](https://github.com/diegosouzapw/OmniRoute/issues/9511))

View File

@@ -0,0 +1 @@
- feat(plugins): expose client request headers in plugin onRequest/onResponse context (#9570)

View File

@@ -0,0 +1 @@
- **feat(audio):** Soniox STT + TTS provider (`sx`) — async speech-to-text (`stt-async-v5`, `stt-async-v4`) and real-time text-to-speech (`tts-rt-v1`) ([#9579](https://github.com/diegosouzapw/OmniRoute/pull/9579))

View File

@@ -0,0 +1,2 @@
- fix(tests): make machineId tests macOS-compatible by stubbing ioreg in test helper (#8577)
- fix(scripts): replace bash 4+ readarray with compatible while-read loop in restore-policies.sh (#8577)

View File

@@ -0,0 +1 @@
- fix(cli): enable systray2 on Windows for Norton-friendly tray (#8609)

View File

@@ -0,0 +1 @@
- fix(providers): gate premium opencode-zen/opencode-go models behind an API key (#8681)

View File

@@ -0,0 +1 @@
- fix(yuanbao-web): accept `content` field in SSE text events (upstream format change) (#8739)

View File

@@ -0,0 +1 @@
- fix(build): remove misleading open-sse/package.json facade and add workspace typecheck gate (#8781)

View File

@@ -0,0 +1 @@
- fix(errorClassifier): classify ChatGPT Web SENTINEL_BLOCKED 403 as terminal FORBIDDEN, enabling proper combo fallback (#8813)

View File

@@ -0,0 +1 @@
- fix(cli): fall back to node:sqlite when better-sqlite3 constructor throws at runtime (#8826)

View File

@@ -0,0 +1 @@
- fix(opencode): prefix provider id with "opencode-" for auth login command (#8830)

View File

@@ -0,0 +1 @@
- fix(opencode-zen): add current free-tier models to registry to enable combo context pre-filtering (#8841)

View File

@@ -0,0 +1 @@
- **fix(opencode):** generate schema-complete model limits so OpenCode accepts catalog entries without an explicit output cap ([#8869](https://github.com/diegosouzapw/OmniRoute/pull/8869)) — thanks @xiaoyaner0201

View File

@@ -0,0 +1 @@
- **fix(cli):** default omitted Codex CLI wire API settings to Responses and clear stale Chat state after reset ([#8876](https://github.com/diegosouzapw/OmniRoute/pull/8876)) — thanks @xiaoyaner0201

View File

@@ -0,0 +1 @@
- **fix(proxy):** isolate new proxy credential fields from browser and password-manager autofill after form reset ([#8883](https://github.com/diegosouzapw/OmniRoute/pull/8883)) — thanks @xiaoyaner0201

View File

@@ -0,0 +1 @@
- **fix(providers):** expose both OAuth Connect and manual API-key actions for dual-auth providers such as CodeBuddy CN ([#8921](https://github.com/diegosouzapw/OmniRoute/pull/8921)) — thanks @Llliao1113

View File

@@ -0,0 +1 @@
- fix(compression): drop orphan custom_tool_call/local_shell_call/apply_patch_call on compaction restore (#8946)

View File

@@ -0,0 +1 @@
- fix(opencode): propagate vision capability from live catalog into opencode.json (#8960)

View File

@@ -0,0 +1 @@
- fix(providers): switch Antigravity quota RPCs to iterate ANTIGRAVITY_RUNTIME_BASE_URLS (#8965)

View File

@@ -0,0 +1 @@
- fix(vertex): route Claude models to native rawPredict endpoint and respect custom targetFormat overrides (#8994)

View File

@@ -0,0 +1 @@
- fix(proxies): resolveProxyForConnection now returns the proxy name, so the dashboard badge shows the name instead of the hostname (#8995)

View File

@@ -0,0 +1 @@
- fix(cursor): preserve tool context across multi-turn conversations when client lacks conversation_id (#9029)

View File

@@ -0,0 +1 @@
- fix(sse): move Antigravity client system content to first user message to avoid upstream 429 RESOURCE_EXHAUSTED on oversized systemInstruction (#9030)

View File

@@ -0,0 +1 @@
- fix(db): stream DB backup export instead of buffering entire file into memory (#9045)

View File

@@ -0,0 +1 @@
- fix(ui): normalize Free Pool API response payload to read from data.proxies (#9046)

View File

@@ -0,0 +1 @@
- fix(api/analytics): stop charging :free models at arbitrary fallback price (#9054)

View File

@@ -0,0 +1 @@
- **fix(batches):** `GET /v1/batches` now validates the `limit` query param instead of passing `Number.parseInt(limit)` straight to the SQLite `LIMIT` bind. Previously `?limit=abc` threw an unhandled `datatype mismatch` (→ HTTP 500), `?limit=-1`/`0` returned an incoherent `has_more:true` empty page with `last_id:null`, and a large `?limit` read the entire `batches` table into memory. It now returns a `400` for any non-integer or out-of-range value (1100, default 20), matching the `POST` handler's Zod validation and the OpenAI Batches contract ([#9073](https://github.com/diegosouzapw/OmniRoute/pull/9073))

View File

@@ -0,0 +1 @@
- **fix(a2a):** the A2A JSON-RPC router now compares the bearer token against `OMNIROUTE_API_KEY` in constant time (`crypto.timingSafeEqual`) instead of `===`, closing a token-length timing side-channel, and no longer logs the request URL to server logs on every call ([#9083](https://github.com/diegosouzapw/OmniRoute/pull/9083))

View File

@@ -0,0 +1 @@
- **fix(api/skills):** the `/api/skills/**` routes now run caught error messages through `sanitizeErrorMessage` before returning them, so a filesystem failure no longer leaks an absolute path (e.g. `/home/<user>/.omniroute/skills/...`) to the client; the `{ error: string }` response shape is preserved for the dashboard ([#9088](https://github.com/diegosouzapw/OmniRoute/pull/9088))

View File

@@ -0,0 +1 @@
- fix(providers): modal.com validation returns clear error when Base URL is missing, instead of leaking "Invalid outbound URL" (#9102)

View File

@@ -0,0 +1 @@
- fix(management): authorize mcp:connect-only keys on loopback/LAN when requireLogin is enabled (#9159)

View File

@@ -0,0 +1,2 @@
- fix(catalog): repair dead guard and synced-first ordering for custom model Vision capable override (#9195)
- fix(routing): consult customModels supportsVision flag in Combo vision filter (#9195)

View File

@@ -0,0 +1 @@
- fix(web-search): bind each search provider attempt to its connection proxy (#9201)

View File

@@ -0,0 +1 @@
- fix(auth): make antigravity and agy equivalent in credential selection (#9204)

View File

@@ -0,0 +1 @@
- fix(lmarena): emit Uint8Array SSE chunks instead of strings to satisfy shared pipeline contract (#9237)

View File

@@ -0,0 +1 @@
- **fix(resilience):** Enforce RPM limits with rolling leases and atomic global/provider/account admission ([#9259](https://github.com/diegosouzapw/OmniRoute/pull/9259)). The configured global RPM budget is shared across all enabled provider connections within one process; provider/account overrides add narrower scopes.

View File

@@ -0,0 +1 @@
- fix(qoder): include actionable CLI_QODER_BIN hint in connection test when qodercli is not found (#9277)

View File

@@ -0,0 +1 @@
- **fix(providers):** the web search fallback detector in `webSearchFallback.ts` used an exact `Set` (`web_search`, `web_search_preview`) that missed Anthropic's date-suffixed server-tool variant `web_search_20250305` (sent by Claude Code 2.1.220+). Changed to prefix regex `/^web_search/`, matching the two other detectors in the codebase, so the fallback intercepts versioned web search tools for OpenAI-compatible upstreams ([#9279](https://github.com/diegosouzapw/OmniRoute/pull/9279))

View File

@@ -0,0 +1 @@
- fix(credential-health): scheduler never retries failed connections due to static interval comparison (#9289)

View File

@@ -0,0 +1 @@
- fix(api): specialty model catalog ignores hidden OpenRouter model flags (#9293)

View File

@@ -0,0 +1 @@
- fix(catalog): cache getModelsDevPricing() to prevent OOM at startup (#9300)

Some files were not shown because too many files have changed in this diff Show More