Compare commits

..

184 Commits

Author SHA1 Message Date
Diego Rodrigues de Sa e Souza
00b79b5507 test(combo): guard auto/best-free never leaks the combo name as a model (#7754)
Refs: base-red #9737
2026-08-08 13:52:44 -03:00
Diego Rodrigues de Sa e Souza
b5e17bdbde fix(resilience): set rateLimitedUntil on connection test failure for recovery (#9623)
Refs: base-red #9737
2026-08-08 13:52:37 -03:00
Diego Rodrigues de Sa e Souza
aefa2b665b fix(playground): surface provider model loading errors in LlmChatCard (#9626)
Refs: base-red #9737
2026-08-08 13:52:30 -03:00
Diego Rodrigues de Sa e Souza
df1ea5bd77 fix(db): align domain_cost_history cleanup cutoff with millisecond column (#9625)
Refs: base-red #9737
2026-08-08 13:52:23 -03:00
Diego Rodrigues de Sa e Souza
a90c5e5aba fix(db): wire telemetry cleanup scheduler in Next.js startup path (#9624)
Refs: base-red #9737
2026-08-08 13:52:17 -03:00
Diego Rodrigues de Sa e Souza
c88b96244f fix(providers): classify 400 out of extra usage as quota_exhausted for Anthropic OAuth (#9486)
Refs: base-red #9737
2026-08-08 13:52:11 -03:00
Diego Rodrigues de Sa e Souza
93ee4dce9f fix(build): add build-next-isolated.mjs sibling imports to package.json files array (#9633)
Refs: base-red #9737
2026-08-08 13:52:04 -03:00
Diego Rodrigues de Sa e Souza
6c95e2b354 fix(build): include better-sqlite3 prebuilds in standalone bun bundle (#8847)
Refs: base-red #9737
2026-08-08 13:51:57 -03:00
Diego Rodrigues de Sa e Souza
3835f318d0 fix(cli): use process.execPath for macOS launchd autostart (#9156)
Refs: base-red #9737
2026-08-08 13:51:51 -03:00
Milan Soni
d0047ee615 fix(sse): enforce capabilities for gemini-web reasoning and tools (#9356) (#9397)
Merge-train validated
2026-08-08 12:47:48 -03:00
小妍儿 ✨
064a19b2d1 fix(quota): clean managed combos when deleting pools (#8906)
Merge-train validated
2026-08-08 12:45:00 -03:00
Diego Rodrigues de Sa e Souza
492f9ddc4a fix: buffer and normalize Responses tool-call argument deltas, stripping optional null before reaching the client (#9168)
Refs: base-red #9737
2026-08-08 12:07:24 -03:00
Diego Rodrigues de Sa e Souza
e7d9055314 fix(translator): avoid double-normalizing tool names in Gemini-to-Claude response path (#9177)
Refs: base-red #9737
2026-08-08 12:07:14 -03:00
Diego Rodrigues de Sa e Souza
2ed487583b fix(model-discovery): ingest capabilities.effort_tiers for synced models (#9160)
Refs: base-red #9737
2026-08-08 12:03:24 -03:00
Diego Rodrigues de Sa e Souza
a6b3b4f57a fix(base-red): strip conflict markers from triage-bugs test file (#9142 merge artifact)
Refs: base-red #9737
2026-08-08 12:01:41 -03:00
Diego Rodrigues de Sa e Souza
cf31b795b7 fix(background): detect Anthropic top-level system prompts for background task detection (#9142)
Refs: base-red #9737
2026-08-08 11:45:54 -03:00
Diego Rodrigues de Sa e Souza
6b706f6b5e fix(sse): broaden OMNIROUTE_SSE_COMMENTS to accept 'false','0','no' and gate metadata comment emission (#9305)
Refs: base-red #9737
2026-08-08 11:35:46 -03:00
Diego Rodrigues de Sa e Souza
8706e717a5 fix(vscode): allow built-in auto-routing models in VS Code model filter (#9140)
Refs: base-red #9737
2026-08-08 11:35:18 -03:00
Diego Rodrigues de Sa e Souza
c960b091a2 fix(lmarena): encode SSE stream chunks as Uint8Array to prevent TextDecoder TypeError (#9306)
Closes #9306
Refs: base-red #9737
fix/9306-arena-ai-is-not-working
2026-08-08 11:27:02 -03:00
Diego Rodrigues de Sa e Souza
9c343237d3 fix(management): authorize mcp:connect-only keys on loopback/LAN when requireLogin is enabled (#9159)
Closes #9159
Refs: base-red #9737
fix/9159-mcp-connect-require-login-lo
2026-08-08 11:26:49 -03:00
Diego Rodrigues de Sa e Souza
3cae1b1480 fix(api): auto/* routing aliases bypass API-key allowedConnections/disableNonPublicModels (#9057)
Closes #9057
Refs: base-red #9737
fix/9057-api-auto-routing-aliases-byp
2026-08-08 11:26:41 -03:00
Diego Rodrigues de Sa e Souza
4095cc0532 fix(api): use configured prefix for alias-backed model id in /v1/models (#9034)
Closes #9034
Refs: base-red #9737
fix/9034-api-custom-openai-compatible
2026-08-08 11:26:34 -03:00
Diego Rodrigues de Sa e Souza
29439c9b11 fix(classify429): add missing 'exhausted their quota' pattern to prevent combo fallback failure (#9269)
Closes #9269
Refs: base-red #9737
fix/9269-auto-coding-does-not-fall-ba
2026-08-08 11:26:24 -03:00
Diego Rodrigues de Sa e Souza
57ee73451c fix(providers): resolve combo names in audio transcriptions route so /v1/models stays honest (#9134)
Closes #9134
Refs: base-red #9737
fix/9134-c-program-files-git-v1-audio
2026-08-08 11:26:05 -03:00
Diego Rodrigues de Sa e Souza
faffd0aa31 fix(providers): admit audio-speech/audio-transcriptions apiType in audio route provider-node filters (#9096)
Closes #9096
Refs: base-red #9737
fix/9096-audio-speech-transcriptions-
2026-08-08 11:25:45 -03:00
Diego Rodrigues de Sa e Souza
670e8314cc fix(translator): thread model through normalizeResponsesReasoningEffort in promotion path (#8997)
Closes #8997
Refs: base-red #9737
fix/8997-gpt56-max-reasoning-rewritte
2026-08-08 11:25:37 -03:00
Diego Rodrigues de Sa e Souza
e545e68a68 fix(github): add targetFormat to GPT-5.6 Sol/Terra/Luna models (#8951)
Closes #8951
Refs: base-red #9737
fix/8951-github-copilot-gpt56-respons
2026-08-08 11:25:30 -03:00
Diego Rodrigues de Sa e Souza
08d1809b6e fix(ci): aggregate fast-gates into non-fail-fast loop so one red gate no longer masks later gates (#8542)
Closes #8542
Refs: base-red #9737
fix/8542-ci-base-red-compounds-becaus
2026-08-08 11:25:24 -03:00
Diego Rodrigues de Sa e Souza
c7e20e95de fix(memory): register the sqlite backend on the /api/memory/[id] route — every handler 500'd (#9737) (#9785)
* fix(api): validate request bodies with Zod in 4 routes — restores the t06 gate

The release-green verdict (#9737) lists check:route-validation:t06 as a HARD
failure and it is STILL red on the current tip: four routes call
request.json() and hand-roll `typeof x === "string"` checks instead of using
Zod, which Hard Rule #7 requires and the gate enforces (it scans source and
has no allowlist).

- src/app/api/plugins/marketplace/install (#9445): InstallBodySchema; the
  400 'Missing or invalid name field' response is preserved verbatim.
- src/app/api/services/dario/admin/accounts (#8523): DeleteAccountBodySchema
  for the optional { alias } DELETE body; query-param path untouched.
- src/app/api/services/dario/admin/login-start (#8523): LoginStartBodySchema;
  trimming now happens in the schema, so the forward body is unchanged.
- src/app/api/services/dario/admin/import-from-omniroute (#8523):
  ImportBodySchema for connectionId/alias; invalid shapes fall back to the
  same 'connectionId is required' 400 as before.

All four keep their exact status codes and messages — this is a validation
mechanism swap, not a contract change (plugins route suite still 33/33).

Adds tests/unit/route-body-validation-t06.test.ts, which runs the gate's own
rule inside the unit suite so the next such route fails on ITS OWN PR instead
of surfacing weeks later in a base-red sweep. Guard verified by mutation:
renaming .safeParse( in one route makes it fail (1 fail), restored from a
pre-probe copy.

Gates: route-validation:t06, file-size, test-discovery, mutation-test-coverage,
dead-code exit 0; typecheck:core clean; eslint clean.

Refs #9737

* fix(memory): register the sqlite backend on the /api/memory/[id] route — every handler 500'd

GET/PUT/DELETE /api/memory/[id] threw `Primary backend "sqlite" not
registered` and returned 500. #8752 (MemoryBackend provider pattern) wired the
route to `@/lib/memory/manager` directly, but the registry is populated by an
import-time side effect in the module INDEX (src/lib/memory/index.ts:23,
`memoryManager.register(sqliteBackend)`). Importing the bare manager gives an
empty registry.

In production the failure is order-dependent, which is why it went unnoticed:
if /api/memory (which imports the index) is hit first in the same process, the
singleton is already populated and [id] works. Reached first — the common case
for a client that edits a known memory id — every request 500s. The sibling
route is the only other consumer and already imports the index; this was the
lone direct-manager import in src/.

- Fix: import from `@/lib/memory` (index) with a comment stating WHY the
  indirection matters, so the next refactor does not simplify it back.
- Guard: tests/integration/memory-route-put.test.ts already covered this and
  was failing 2/5 on the base (it only surfaced now because the integration
  suite runs on the release-PR CI, not per-PR). Now 5/5.

Also fixes a test-isolation defect in the same run:
tests/integration/combo-matrix/context-relay-codex.test.ts reused one combo
name across both tests, and the control failed with `UNIQUE constraint failed:
combos.name` — resetStorage() unlinks the DB file but the previous
better-sqlite3 handle keeps writing to the same inode. Gave the control its own
combo name and parameterized the request builder; the assertion is unchanged
(it never depended on the name). 2/2.

Integration suite on this tip: 936 tests, 32m19s — under the 40min ceiling the
old verdict reported as exceeded (#9737 item 6), which the migration-135
collision was causing.

Refs #9737

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-08 11:23:45 -03:00
Diego Rodrigues de Sa e Souza
24bdae29ca fix(api): validate request bodies with Zod in 4 routes — restores the t06 gate (#9779)
The release-green verdict (#9737) lists check:route-validation:t06 as a HARD
failure and it is STILL red on the current tip: four routes call
request.json() and hand-roll `typeof x === "string"` checks instead of using
Zod, which Hard Rule #7 requires and the gate enforces (it scans source and
has no allowlist).

- src/app/api/plugins/marketplace/install (#9445): InstallBodySchema; the
  400 'Missing or invalid name field' response is preserved verbatim.
- src/app/api/services/dario/admin/accounts (#8523): DeleteAccountBodySchema
  for the optional { alias } DELETE body; query-param path untouched.
- src/app/api/services/dario/admin/login-start (#8523): LoginStartBodySchema;
  trimming now happens in the schema, so the forward body is unchanged.
- src/app/api/services/dario/admin/import-from-omniroute (#8523):
  ImportBodySchema for connectionId/alias; invalid shapes fall back to the
  same 'connectionId is required' 400 as before.

All four keep their exact status codes and messages — this is a validation
mechanism swap, not a contract change (plugins route suite still 33/33).

Adds tests/unit/route-body-validation-t06.test.ts, which runs the gate's own
rule inside the unit suite so the next such route fails on ITS OWN PR instead
of surfacing weeks later in a base-red sweep. Guard verified by mutation:
renaming .safeParse( in one route makes it fail (1 fail), restored from a
pre-probe copy.

Gates: route-validation:t06, file-size, test-discovery, mutation-test-coverage,
dead-code exit 0; typecheck:core clean; eslint clean.

Refs #9737

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-08 10:35:18 -03:00
Diego Rodrigues de Sa e Souza
63cf354129 fix(docker): complete partially traced packages in standalone co-location (#9615)
* fix(docker): complete partially traced packages in standalone co-location

Publish-to-Docker-Hub has failed on every release/v3.8.50 push since #9151
enabled publishing from active release branches: the post-build guard dies
with "Cannot find module .../@atjsh/llmlingua-2/dist/index.js" while the
co-location step right above it reports 100 packages copied.

Root cause: Next's file tracing materializes @atjsh/llmlingua-2 PARTIALLY
in the standalone (package.json lands, the dist/ payload its main points at
does not). colocateOptionals' no-clobber checked existsSync on the package
DIRECTORY, so the partial shell counted as present and the one package that
mattered was skipped forever (#9185 added the closure walk but kept the
directory-level check).

Fix: presence is now judged by entrypoint integrity — the package resolves
from inside the target tree (same contract as the Dockerfile guard). Partial
directories are completed with a file-level no-clobber merge (cpSync
force:false), so files the trace did materialize are never overwritten and
pinned instances (dist transformers 3.5.2) keep their protection.

Validation (TDD): 2 new tests in docker-llmlingua-optionals-9166.test.ts
reproduce the CI failure (partial package skipped; closure-wide early-exit
firing while a member is partial) — red on the old code, 5/5 green after.

* fix: update colocate test mock packages to match isPackageIntact entrypoint resolution

The PR's isPackageIntact check uses require.resolve to validate that
co-located packages have a usable entrypoint inside the target tree.
The pre-existing test's mock packages lacked main fields and index
files, so require.resolve failed and the idempotency assertion broke.

Update buildRoot() to give every closure package a resolvable entry
(main + index.js), mirroring what real npm packages ship.

Refs #9615

* docs(changelog): fragment for #9615

* fix(yuanbao-web): accept content field in SSE text events (upstream format change) (#8739)

Closes #8739

* fix(errorClassifier): classify ChatGPT Web SENTINEL_BLOCKED 403 as FORBIDDEN, enabling combo fallback (#8813)

Closes #8813

* fix(vertex): route Claude models to native rawPredict and respect targetFormat overrides (#8994)

Closes #8994

* fix(cursor): preserve tool context across multi-turn conversations when client lacks conversation_id (#9029)

Closes #9029

* fix(sse): move Antigravity client system content to first user message to avoid upstream 429 (#9030)

Closes #9030

* fix(combo): distinguish pre-dispatch skips from genuine failures to prevent false 503 ALL_ACCOUNTS_INACTIVE (#9630)

Closes #9630

* fix: repair stray brace in combo.ts and fix no-explicit-any types in repro-9630 test

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-08 09:41:46 -03:00
Diego Rodrigues de Sa e Souza
9e5fca685c test(cli): realign opencode-plugin suite to the bare-key static-catalog contract (#9614)
* 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).

* fix(pr): fix changelog fragment format, login-bootstrap test assertions, and VM_DEPLOYMENT_GUIDE fabricated env vars

* fix(pr): remove YAML frontmatter from changelog fragment (validator expects bare bullet)

* fix(pr): update file-size baseline for base-red drift after merging 48 base commits

* fix(pr): rename duplicate migration 134_proxy_logs_egress_ip to 139

* docs(changelog): fragment for #9614

* Revert "fix(pr): rename duplicate migration 134_proxy_logs_egress_ip to 139"

This reverts commit 1312e1a917.

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-08 09:40:52 -03:00
Diego Rodrigues de Sa e Souza
36abd86929 fix(ci): clear the 08-08 base-red layers — dead-code, prod crash in chat.ts, Responses payload regression, born-red stdio test, gate drifts (#9757)
* fix(ci): drop unused RadarReferrals type export — dead-code ratchet back to 227 baseline

The radar referral-links feature (#9697) exported the inferred type
RadarReferrals from feedSchema.ts but nothing imports it (the singular
RadarReferral is the consumed type). knip counts it as a new dead export,
pushing the dead-code ratchet to 228 > 227 and failing Fast Quality Gates
on every PR born after the merge. RadarReferralsSchema itself stays — it
is used by RadarFeedSchema.

Refs #9737

* fix(ci): clear the 08-08 base-red layer — prod crash in chat.ts, Responses API payload regression, born-red stdio test, gate drifts

Six independent base-reds from the 08-07 evening merge batch, each verified
against the pure release/v3.8.50 tip:

- src/sse/handlers/chat.ts: #9467's squash carried a refactor hunk that
  renamed the all-rate-limited breaker guard to an UNDEFINED variable
  (isAllRateLimited) — a production ReferenceError on the all-accounts-429
  path (chat.ts is outside typecheck:core scope, so only tests caught it).
  Restore credentials?.allRateLimited. Guard: chat-rate-limit-body-lock (2/2),
  also un-breaks batch_api and chat-combo-live-test.
- open-sse/utils/stream.ts: #9315 switched providerPayload summaries to the
  accumulated responseBody, but in passthrough paths that body is synthesized
  in chat-completion shape — Responses API lost its `response` object in the
  dashboard payload. Keep the events-derived summary for OPENAI_RESPONSES
  only. Guard: stream-utils + stream-collector-9315 suites (51/51).
- tests/unit/mcp-stdio-json-purity.test.ts: born red — the full CLI chain
  takes ~10s (2x tsx import + DB init) and the test slept a fixed 4s. Poll
  for the first stdout line with a 60s deadline instead.
- tests/unit/plugins-route-error-sanitization.test.ts: register #9445's new
  marketplace/install route in PLUGIN_ROUTES (route already sanitizes) (33/33).
- tests/unit/provider-models-route-codex.test.ts: realign pinned GPT-5.6
  input limit to #9432's deliberate 272000→922000 bump (7/7).
- lint: fix 11 no-explicit-any errors in repro-9630 + specialty-9293 tests,
  prune 1 orphaned suppression, allowlist the opencode-ai devDependency
  (#8869, publisher-verified), and reword a doc line the fabricated-docs
  gate misread as an env var.

Gates re-verified locally: lint:json --max-warnings 0 exit 0, dead-code 227,
typecheck:core clean, check:deps OK, check:fabricated-docs OK.

Refs #9737

* fix(ci): clear the third 08-08 base-red layer — invalid ru rule pack, stale event pin, orphaned UI repro test, pack/mutation/file-size drifts

Follow-up to the previous layer: the serial fast-gates chain unmasked one
more stratum after file-size/dead-code went green, all verified against the
merged release/v3.8.50 tip:

- compression rules ru/ultra.json (#9581): two rules shipped
  minIntensity "notes", which is not a valid CavemanIntensity
  (lite|full|ultra) — loading ANY language pack list threw and killed the
  rtk-loader suite. Mapped both to "ultra" (they are the most aggressive
  punctuation/case rules, matching the en pack tiers). 2/2.
- plugins-welcome-banner-e2e: #9668 added the onStreamComplete builtin
  event (real emission path via runOnStreamCompleteHooks) and missed this
  pinned-list sibling. 35/35.
- tests/unit/free-pool-frontend-repro (#9046): landed as .tsx with
  node:test semantics — no runner collects tests/unit/*.tsx, so it NEVER
  ran (test-discovery NEW-orphan). It contains zero JSX; renamed to .test.ts
  so the unit runner's existing glob collects it. 5/5 (first real run).
- pack-policy: allow + require bin/mcpStdioConsoleGuard.mjs (#9281) — it is
  preloaded via node --import by bin/mcp-server.mjs, so a published artifact
  without it crashes 'omniroute --mcp' at startup.
- stryker.conf.json: add 5 covering unit tests from the batch (#8779/#9204/
  #9330/#9630/openrouter-passthrough) to tap.testFiles (--strict drift).
- file-size-baseline: consolidate the base-drift rebaseline for the 12
  files grown by the 08-06..08-08 batches (#9616's entries never reached the
  base; measured on this branch's tree — this PR's own source edits add zero
  lines to any frozen file).

Local battery: file-size/deps/test-discovery/mutation/pack-policy/dead-code/
duplication/docs-all/secrets/vuln/workflows ratchets all exit 0; full lint
gate --max-warnings 0 exit 0.

Refs #9737

* fix(types): clear the 3 uncovered open-sse-typecheck regressions + realign combo skip-code siblings

Fourth base-red layer unmasked by the serial gates. The other 4 typecheck
regressions (codex.ts, kiro.ts, tierResolver.test.ts, translator/index.ts)
already have dedicated open [TS7] PRs (#9748/#9753/#9742/#9747) — not
duplicated here. This commit covers only what no open PR owns:

- devin-agentic/serializer.ts TS2367: drop the dead 'role === "system"'
  branch — the guard above already narrows role to user|assistant (system
  throws unsupported_role). Devin suites 104/104.
- raycast.ts TS2416: the buildHeaders 'override' never matched the base
  signature (2nd param is the signed payload string, not the stream
  boolean) — renamed to a private buildRaycastRequestHeaders helper so a
  polymorphic buildHeaders(credentials, true) call can never bind here.
- modelMetadataRegistry.ts TS2352: PricingByProvider → nested-record cast
  now goes through unknown (shape is runtime-guarded by findInsensitive).
- combo-routing-engine.test.ts: realign 2 pre-dispatch-skip expectations to
  #9630's deliberate ALL_TARGETS_SKIPPED contract (87/87).

Refs #9737

* fix(ci): clear the fifth 08-08 base-red layer — reasoning-placeholder contract sweep, GPT-5.6 limits sweep, vi key parity

The 08-08 merges (#9610 reasoning replay, #9432 GPT-5.6 limits, #9630 combo
skip codes, #9336 provider key links) each changed a contract and left
sibling tests pinning the old one. Full grep sweep per contract, not just
the shard that happened to go red:

- reasoning placeholder (#9573/#9610): the fix DELIBERATELY removed
  NON_ANTHROPIC_THINKING_PLACEHOLDER injection on cache miss — the model
  echoed the placeholder as its own reasoning (empty stop) and re-poisoned
  cache + client history; DeepSeek's 400 is specific to an EMPTY STRING, not
  an absent field. Realigned reasoning-cache (2 cases, renamed to describe
  omission) + tool-request-sanitization (1 case + dead import). 60/60.
- GPT-5.6 Codex limits (#9432, 272000 -> 1050000 ctx / 922000 input):
  realigned vscode-token-routes-gpt56 (2) + vscode-token-routes (3). 43/43
  together with t23-t24.
- combo skip codes (#9630): t23-t24-fallback-resilience T24 now expects
  ALL_TARGETS_SKIPPED like the combo-routing-engine siblings.
- vi.json key parity: #9336 added providers.getApiKey/getApiKeyDescription
  to en.json without syncing vi (the only locale with a parity gate).
  Translated both; providers block reordered to match en key order. 5/5.
- pack-artifact-policy.test.ts: sibling of this PR's own required-paths
  change (bin/mcpStdioConsoleGuard.mjs). 10/10.
- combo-routing-engine.test.ts: dropped the 6 comment lines added in the
  previous commit so the frozen test file-size stays at its baseline (the
  rationale lives in that commit message, not the test body).

Gates: file-size, test-discovery, mutation-test-coverage, pack-policy,
open-sse-typecheck, dead-code all exit 0.

Refs #9737

* fix(translator): keep the reasoning_content placeholder for Xiaomi MiMo — #9610 traded one live 400 for another

The xiaomi-mimo replay test (9router#1321) went red on the base after #9610
removed the NON_ANTHROPIC_THINKING_PLACEHOLDER injection globally. That test
is NOT stale — it guards a documented upstream 400 ('Param Incorrect: The
reasoning_content in the thinking mode must be passed back to the API'), so
realigning it would have masked a reintroduced production bug.

Two real bugs conflict here:
- #9573: forwarding the placeholder makes the model continue its chain of
  thought FROM that text (echo -> empty stop) and re-poisons cache/history.
- 9router#1321/#1337: omitting reasoning_content on a plain replay turn makes
  Xiaomi MiMo reject the request outright.

#9610's evidence for omitting is provider-specific — it verified that
deepseek-v4-flash accepts an ABSENT field. It does not extend to MiMo. So the
omission stays for every provider #9610 covered, and the placeholder survives
the cache miss only for xiaomi-mimo (new requiresReasoningContentPresence
predicate next to isReasoningOnlyReplayTarget). The echo that comes back is
still stripped on the way in by isInternalReasoningPlaceholder(), so #9573's
cache/history poisoning stays fixed for MiMo too.

Both contracts now hold simultaneously: xiaomi-mimo replay + reasoning-cache +
tool-request-sanitization 61/61; placeholder-strip/responses/translator/combo
regression sweep 168/168. Gates: file-size, open-sse-typecheck, dead-code,
mutation-test-coverage exit 0; typecheck:core clean.

A live check on the VPS (Hard Rule #18 path 2) is the only way to confirm the
DeepSeek half of #9610's empirical claim; flagging it in the PR rather than
widening this fix on speculation.

Refs #9737

* test(translator): pin the reasoning-placeholder provider scope so neither half of the conflict can silently re-break

#9610 removed the placeholder globally on the strength of ONE provider's
observed behavior (deepseek-v4-flash accepting an absent reasoning_content),
which re-opened the MiMo 400 (9router#1321). The previous commit scoped the
placeholder to xiaomi-mimo; this pins BOTH directions in one test so the next
global edit fails loudly instead of trading the bugs again:

- xiaomi-mimo plain replay turn, cache miss -> reasoning_content present
  (narrowing the scope away from MiMo re-opens 9router#1321)
- deepseek plain replay turn, cache miss -> reasoning_content absent
  (widening it back to DeepSeek re-opens the #9573 echo bug)

Guard verified by mutation: forcing requiresReasoningContentPresence() to
return true makes the DeepSeek half fail (1 pass / 1 fail), and the file was
restored from the pre-probe copy before committing.

Also checked kimi-coding/kimi-coding-apikey, the other strict-contract entries
in REASONING_REPLAY_PROVIDERS: their originating PR (#7673) fixes capture and
replay of REAL reasoning and documents no 400 on an absent field, so they stay
out of the placeholder scope — evidence-scoped, not speculatively widened.

Reasoning suites together: 87/87. Gates: file-size, test-discovery,
mutation-test-coverage, dead-code exit 0; eslint clean.

Refs #9737

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-08 09:08:45 -03:00
Diego Rodrigues de Sa e Souza
617e2d9ecc fix(base-red): prune stale ESLint suppression + align combo tests
Clears 'No new ESLint warnings' and combo test failures on release branch.

Refs #9679
2026-08-08 08:45:42 -03:00
diegosouzapw
5199f464c1 fix(combo): align combo-routing tests with #9630 pre-dispatch skip contract
#9630 (976d670ff3) intentionally changed the pre-dispatch skip behavior: when
recordedAttempts === 0 (all targets filtered before any dispatch), handleComboChat
now returns 503 ALL_TARGETS_SKIPPED instead of the misleading ALL_ACCOUNTS_INACTIVE.

The two combo-routing-engine tests covering the 'every target skipped before
execution' scenario still asserted the old code. Align both assertions to
ALL_TARGETS_SKIPPED (the tests still verify the 503 + meaningful error code).
2026-08-08 08:44:52 -03:00
diegosouzapw
2e1320796e fix(quality): prune stale ESLint suppression for search.ts
The no-explicit-any count for open-sse/handlers/search.ts dropped from 34
to 33 (an any was removed upstream). Prune the stale suppression to clear
the 'No new ESLint warnings' gate on the release branch.
2026-08-08 08:42:31 -03:00
Diego Rodrigues de Sa e Souza
a1c864373a referrals from standalone /v1/referrals feed (no 30-day delay) (#9762)
* feat(radar): sync referral links from standalone /v1/referrals/latest feed

Referral links previously came from the catalog feed cache, which on the
community tier can be up to 30 days stale -- a newly-added referral would
not reach a free/community user for up to a month. Adds a new sync module
(syncRadarReferrals), Ed25519-verified feed schema, and a dedicated
radar_referrals_cache table (migration 142) so referrals sync on their own,
much shorter cadence instead of inheriting the catalog's delay.

getRadarReferrals()/getDefaultReferralFor() now read the new cache instead
of the catalog feed's embedded referrals field (kept on RadarFeedSchema for
backward-compat with already-cached catalog feeds, but no longer read).

* feat(radar): wire sync-on-read + scheduler side-sync for referrals

GET /api/radar/referrals now triggers syncRadarReferrals() inline whenever
the cache is missing or older than 1h (shouldSyncReferralsOnRead), so fixed
links show up promptly on the next dashboard load instead of waiting on a
background timer. The route itself still never talks to the upstream feed
server directly -- syncRadarReferrals() remains the only network touchpoint.

radarSchedulerTick() also evaluates referrals staleness on the same hourly
tick used for the catalog, independent of the catalog's own due-ness, as a
best-effort side effect that never changes RadarTickResult's shape and is
swallowed on error.

* docs(radar): document the standalone referrals feed sync

Explains the /v1/referrals/latest feed, its no-tier-field-in-body design
(x-omniroute-feed-tier header is the only tier source), the sync-on-read +
scheduler side-sync triggers, and the self-hosting note for forks that only
serve the catalog feed.

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-08 08:10:20 -03:00
Diego Rodrigues de Sa e Souza
0d7c019eec campo de colar chave omr_ na tela de ativação (#9758)
* feat(radar): shared supporter-key format validator

Extract the "omr_" + 40 hex supporter-key regex out of the
POST /api/radar/settings Zod schema into a pure, client-safe helper
(src/lib/radar/supporterKey.ts) so the format rule lives in exactly one
place and the upcoming activation-screen input can reuse it for a
UX-only pre-check. Server-side Zod validation stays authoritative.

Adds regression coverage: both directions of the format check, a
combined opt-in+supporterKey POST persisting both fields with the key
always masked (never raw) in either the POST or GET response body, and
a flag-off inertia case for the same combined payload shape.

* feat(dashboard): paste-key input on the Radar activation screen

The Radar activation screen had opt-in and the two "get a key" claim
buttons, but nowhere to paste a key someone already has — the last
piece of the supporter flow. Add the field to the activation screen
itself, as the primary path: pasting a key and submitting sends
POST /api/radar/settings with { optIn: true, supporterKey } together,
so pasting a valid key both sets it and unlocks the screen in one step.

Client-side format validation (via the shared isValidSupporterKeyFormat
helper) is a UX nicety only; the server's Zod schema already validates
authoritatively. When a key is already set (hasSupporterKey from
GET /api/radar/settings — e.g. set out of band before this UI existed),
the screen shows the masked form instead of an empty input, with a
"change key" control to paste a new one; the raw key is never
displayed. The existing plain "Activate" button (no key, community
tier) and the two claim/plans buttons are unchanged and still present
below, so all three paths to this screen coexist.

Adds 4 new i18n keys (keySectionTitle, keyInvalidFormatError,
activateWithKeyButton, changeKeyButton) with an English fallback across
all 43 locale files (172 entries) — no __MISSING__ sentinel, no price.

* docs(radar): close the paste-key-input known gap

RADAR.md documented a known gap: the activation screen had no
dedicated key-paste input, only the two claim/plans buttons. That gap
is closed — describe the new input, the combined opt-in+supporterKey
submission, and the masked-key "already activated" state instead.

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-08 08:10:14 -03:00
Diego Rodrigues de Sa e Souza
d7a7302ec8 fix(deps): bump nanoid, dompurify, mermaid, js-yaml on default branch
Closes 9 Dependabot alerts (#182-#190). npm audit → 0.
2026-08-08 08:10:11 -03:00
diegosouzapw
19666060b2 fix(deps): bump nanoid, dompurify, mermaid, js-yaml on default branch
Closes 9 Dependabot alerts (#182-#190) on the default branch
(release/v3.8.50):
- nanoid ^3.3.17 (3.3.16→3.3.18)
- dompurify ^3.4.13 (direct dep bump + monaco-editor scoped override)
- mermaid → 11.16.1
- js-yaml v4 nested copies → 4.3.1 (scoped overrides)

npm audit → 0 vulnerabilities.
2026-08-08 08:07:38 -03:00
backryun
d73a1e6fc8 fix(kiro): complete cache-only usage totals (#9753) 2026-08-08 07:15:34 -03:00
backryun
b0a377e66c fix(types): preserve narrowed Codex input arrays (#9748) 2026-08-08 07:15:31 -03:00
backryun
52a6b04f8e fix(types): preserve video URL override contracts (#9747) 2026-08-08 07:15:28 -03:00
backryun
4f8dccc8a3 test(types): use Vitest expectations in tier resolver (#9742) 2026-08-08 07:15:25 -03:00
Diego Rodrigues de Sa e Souza
71c85f31cd feat(guardrails): modality bridge core — vision mode/task-aware/cache/input_image + modalityBridge settings (#9759)
* feat(sse): unified media-part detection helper (image+audio, input_image)

* refactor(guardrails): extractImageParts/comboStructure delegate to unified media detector

* fix(sse): media detector — audio parts no longer shadow sibling/nested image indicators

* fix(guardrails): close extract↔replace contract for input_image (allowlist + splice)

* perf(guardrails): skip media traversal when bridge disabled; short-circuit combo image check

* feat(guardrails): in-memory LRU bridge cache (sha256 keyed)

* feat(settings): modalityBridge* schema with legacy visionBridge* fallback

* feat(db): migrate visionBridge* settings to modalityBridge* (idempotent)

* refactor(guardrails): harden bridge cache key/config + settings resolution (review minors)

* feat(guardrails): vision bridge mode selector (auto/describe/reroute) short-circuit

* feat(guardrails): task-aware vision description prompt (default on)

* feat(guardrails): describe-path cache integration

* docs(guardrails): review polish — cache-key coupling notes + helper header

* feat(guardrails): in-memory bridge stats + modality-bridge response header

* feat(api): modality bridge stats endpoint + header wiring in chat handler

* docs(guardrails): document modality bridge mode/task-aware/cache/header + stats endpoint

* chore: untrack _tasks symlink (inherited from base tip; blocks pre-commit tracked-artifacts gate)

* fix(db): renumber modality bridge migration 139->140 (base renumbered ccr_blocks to 139)

* docs(guardrails): migration filename touch-up 139->140

* docs(db): stale comment touch-ups after 139->140 renumber and #9688 landing

* fix(db): renumber modality bridge migration 140->141 (base renumbered connection_runtime_state to 140)

* test(db): migration test titles 139->141

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-08 01:59:11 -03:00
diegosouzapw
caf768e3c4 fix(repo): untrack the _tasks self-referential symlink
_tasks is a SEPARATE nested git repo (gitignored). A self-referential symlink
_tasks -> its own path was tracked here; every pull materialized it over the real
_tasks repo, destroying plans/specs/hands-off. Now untracked (and /_tasks in
.gitignore prevents re-capture).
2026-08-08 01:21:02 -03:00
diegosouzapw
fd0e8da8ff fix(repo): harden .gitignore to also ignore a _tasks symlink (/_tasks) 2026-08-08 01:20:35 -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
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
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
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
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
833 changed files with 41960 additions and 5516 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
# ─────────────────────────────────────────────────────────────────────────────
@@ -1509,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)
@@ -1912,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.
@@ -2440,10 +2492,10 @@ QUOTA_STORE_DRIVER=sqlite # sqlite | redis
# ═══════════════════════════════════════════════════════════════════════════════
# 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. Both variables below are optional and
# only needed to point the client at a self-hosted/forked feed instead of the
# default OmniRoute Radar feed. Used by: src/lib/radar/sync.ts,
# src/lib/radar/pinnedKeys.ts.
# 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.
@@ -2453,3 +2505,12 @@ QUOTA_STORE_DRIVER=sqlite # sqlite | redis
# 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

@@ -137,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 }}
@@ -237,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 }}
@@ -372,7 +372,7 @@ 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

View File

@@ -151,63 +151,6 @@ jobs:
key: eslint-${{ runner.os }}-${{ hashFiles('eslint.config.mjs', 'eslint.complexity-ratchets.config.mjs', 'config/quality/eslint-suppressions.json', 'package-lock.json') }}
restore-keys: |
eslint-${{ runner.os }}-
- run: npm run check:provider-consistency
- run: npm run check:fetch-targets
# docs-all / openapi-routes / docs-symbols live in docs-gates (path-filtered).
- run: npm run check:deps
# #8522: --base-ref mode for PR events — compare against max(frozen, base) so
# inherited drift (base already over frozen cap) doesn't red an innocent PR.
# workflow_dispatch (no PR base) falls back to absolute comparison.
- name: File-size ratchet (base-relative on PR)
env:
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
if [ -n "$PR_BASE_SHA" ]; then
npm run check:file-size -- --base-ref "$PR_BASE_SHA"
else
npm run check:file-size
fi
- run: npm run check:error-helper
- run: npm run check:migration-numbering
- run: npm run check:public-creds
- run: npm run check:db-rules
- run: npm run check:known-symbols
- run: npm run check:route-guard-membership
- run: npm run check:test-discovery
- run: npm run check:test-runner-api
# Guards tap.testFiles drift: a covering unit test absent from stryker.conf.json
# tap.testFiles makes its module's mutants survive on a cold nightly-mutation run,
# false-failing the blocking mutationScore ratchet. See check-mutation-test-coverage.mjs.
- run: npm run check:mutation-test-coverage
- run: npm run check:any-budget:t11
# Build-scope guard: fails if worktrees/cruft leak into the tsconfig include
# scope (would OOM `next build`). Instant. See incident 2026-06-25 / #5031.
- run: npm run check:build-scope
# Pack-policy (unexpected-files allowlist) WITHOUT a build — catches a stray file
# leaking into the npm tarball (v3.8.36: 6 ops bin/*.sh) per-PR instead of only on
# the release PR's heavy Package Artifact job.
- run: npm run check:pack-policy
# Complexity + cognitive-complexity: ONE ESLint walk (both baselines still
# enforced separately by ruleId). Avoids two cold tree walks on fast-path.
- run: npm run check:complexity-ratchets
# ── G0 (trilho .50): gates do trilho A que faltavam no trilho B ──────────────
# The god-file refactor happens in PRs→release/**; without these, the release
# rail never sees a new import cycle, dead code, duplication or a security
# regression until the release PR to main. Deliberately NOT brought here:
# bundle-size (self-skips without a build — this rail's build job is advisory
# and uploads nothing, so it would be dead configuration) and the coverage
# run (fast-unit already runs the full suite; the coverage ratchet stays on
# the main rail via --allow-missing in lint-guard).
- run: npm run check:cycles
- run: npm run check:lockfile
- name: Duplication ratchet
run: npm run check:duplication
- name: Dead-code ratchet (knip)
run: npm run check:dead-code
- name: Type coverage ratchet
run: npm run check:type-coverage
- name: Compression budget ratchet
run: npm run check:compression-budget
# Security scanners — same hardened install as ci.yml quality-extended
# (gh release download = authenticated, 5000 req/hr; curl to api.github.com
# is rate-limited to 60/hr and silently no-ops when throttled). The blocking
@@ -251,26 +194,63 @@ jobs:
"$HOME/.local/bin/osv-scanner" --version || true
"$HOME/.local/bin/oasdiff" --version || true
zizmor --version || true
- name: Secret scan (gitleaks, ratchet, blocking)
run: npm run check:secrets -- --ratchet
- name: Vulnerability ratchet (osv-scanner, ratchet, blocking)
run: npm run check:vuln-ratchet -- --ratchet
- name: Workflow lint (actionlint+zizmor, ratchet, blocking)
run: npm run check:workflows -- --ratchet
# BASE_REF is read by the script from the env (never interpolated into a
# shell body) — workflow-injection-safe. actions/checkout fetches remote
# refs, not a local branch named github.base_ref, so prefix origin/ or this
# gate self-skips every PR with reason=base-unresolved.
- name: OpenAPI breaking-change (oasdiff, ratchet, blocking)
# Quality gates (all, non-fail-fast) — #8542: replaces 17 bare check:* steps,
# 6 G0 gates, 4 ratchet gates, and 3 typecheck steps with a single aggregation
# step. Each gate runs in a loop with ::group::; failures are collected and
# reported at the end. set -uo pipefail (NOT set -e) so one failing gate does
# not abort the job and mask every later gate. Release-added gates are folded
# in: open-sse typecheck (#8781) and file-size base-relative mode (#8522).
- name: Quality gates (all, non-fail-fast)
env:
# #8522: base-relative file-size mode on PR events — inherited drift (base
# already over frozen cap) must not red an innocent PR. Unset on
# workflow_dispatch (no PR base) → absolute comparison.
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
BASE_REF: ${{ github.base_ref && format('origin/{0}', github.base_ref) || '' }}
run: npm run check:openapi-breaking -- --ratchet
- name: Typecheck (core)
run: npm run typecheck:core
# #7033: dashboard-scoped typecheck gate — src/app/(dashboard) TSX is not
# covered by typecheck:core's curated allowlist. See check-dashboard-typecheck.mjs.
- name: Typecheck (dashboard)
run: npm run check:dashboard-typecheck
run: |
set -uo pipefail
gates=(
provider-consistency fetch-targets deps file-size error-helper
migration-numbering public-creds db-rules known-symbols
route-guard-membership test-discovery test-runner-api
mutation-test-coverage any-budget:t11 build-scope pack-policy
complexity-ratchets
cycles lockfile duplication dead-code type-coverage compression-budget
# #8781: open-sse workspace typecheck gate — the workspace imports @/ which
# escapes to src/ via undeclared path aliases. See check-open-sse-typecheck.mjs.
open-sse-typecheck
)
ratchet_gates=(
secrets vuln-ratchet workflows openapi-breaking
)
failed=()
for g in "${gates[@]}"; do
echo "::group::check:$g"
# #8522: file-size is base-relative on PR events (compare against
# max(frozen, base)) so inherited drift doesn't red an innocent PR;
# workflow_dispatch (no PR base) falls back to absolute comparison.
if [ "$g" = "file-size" ] && [ -n "${PR_BASE_SHA:-}" ]; then
npm run "check:$g" -- --base-ref "$PR_BASE_SHA" || failed+=("$g")
else
npm run "check:$g" || failed+=("$g")
fi
echo "::endgroup::"
done
for g in "${ratchet_gates[@]}"; do
echo "::group::check:$g (ratchet)"
npm run "check:$g" -- --ratchet || failed+=("$g")
echo "::endgroup::"
done
echo "::group::typecheck:core"
npm run typecheck:core || failed+=("typecheck:core")
echo "::endgroup::"
echo "::group::check:dashboard-typecheck"
npm run check:dashboard-typecheck || failed+=("check:dashboard-typecheck")
echo "::endgroup::"
if (( ${#failed[@]} )); then
printf '::error::%d gate(s) failed: %s\n' "${#failed[@]}" "${failed[*]}"
exit 1
fi
# 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

9
.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
@@ -260,3 +265,7 @@ docker-compose.yml.bak
# output/**/.playwright-cli/ (covered above), but anything written directly to
# output/ would otherwise show up as untracked.
/output/
# _tasks e um repo git SEPARADO (ver AGENTS.md). _tasks/ (com barra) NAO ignora um
# SYMLINK _tasks; /_tasks (ancorado) cobre symlink/dir na raiz (incidente 2026-08-08).
/_tasks

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,
@@ -2826,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,
@@ -3009,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
@@ -3243,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,
@@ -3318,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
@@ -4135,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`
@@ -4172,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
@@ -4272,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;
}
@@ -4324,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;
});

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 |

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>

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

@@ -52,8 +52,11 @@ export class ServerSupervisor {
// silently, so a boot that never becomes ready looked like a dead hang with zero
// output even at APP_LOG_LEVEL=debug. Pipe stdout too and buffer it alongside
// stderr so a readiness timeout can surface what the child actually printed.
// #9156: macOS launchd cannot resolve bare "node" because its PATH is
// minimal. Always use process.execPath (the absolute path to the running
// Node.js binary) so the supervisor never depends on PATH resolution.
this.child = spawn(
process.versions.bun ? process.execPath : "node",
process.execPath,
process.versions.bun
? [this.serverPath]
: buildNodeRuntimeArgs(process.env, this.memoryLimit, this.serverPath),

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(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 @@
- test(combo): guard auto/best-free never leaks the combo name as a model (#7754)

View File

@@ -0,0 +1 @@
- fix(ci): aggregate all fast-gates into non-fail-fast loop so one red gate no longer masks later gates (#8542)

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

@@ -1 +0,0 @@
- **fix(api):** make `/v1/models` stale refresh response-safe and generation-safe, with narrow synced-model invalidation ([#8728](https://github.com/diegosouzapw/OmniRoute/pull/8728)). Related to #8697.

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(build): include better-sqlite3 prebuilds in standalone bun bundle

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(quota):** Deleting a quota pool now removes its scoped managed combos without racing in-flight pool mutations ([#8906](https://github.com/diegosouzapw/OmniRoute/pull/8906)) — 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(github): add targetFormat to GPT-5.6 Sol/Terra/Luna models (#8951)

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(translator):** the Responses-to-Chat promotion path called `normalizeResponsesReasoningEffort` without the model argument, so GPT-5.6 Sol/Terra/Luna requests with `reasoning.effort: "max""` were downgraded to `"xhigh"`. The model is now threaded through, preserving `max` for GPT-5.6 while keeping the legacy downgrade for older models ([#8997](https://github.com/diegosouzapw/OmniRoute/pull/8997))

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(api): use configured prefix instead of raw node UUID for alias-backed model id in /v1/models (#9034)

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(api): auto/* routing aliases bypass API-key allowedConnections/disableNonPublicModels (#9057)

View File

@@ -0,0 +1 @@
- fix(providers): admit audio-speech/audio-transcriptions apiType in audio route provider-node filters (#9096)

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(providers): resolve combo names in audio transcriptions route so /v1/models stays honest (#9134)

View File

@@ -0,0 +1 @@
- fix(vscode): allow built-in auto-routing models in VS Code model filter (#9140)

View File

@@ -0,0 +1 @@
- fix(background): detect Anthropic top-level system prompts for background task detection (#9142)

View File

@@ -0,0 +1 @@
- fix(cli): use process.execPath for macOS launchd autostart

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 @@
- fix(model-discovery): ingest capabilities.effort_tiers for synced models (#9160)

View File

@@ -0,0 +1 @@
- fix(translator): buffer and normalize upstream tool-call argument deltas so optional null values are stripped before reaching the client (#9168)

View File

@@ -0,0 +1 @@
- fix(translator): avoid double-normalizing tool names in Gemini-to-Claude response path (#9177)

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)

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