* 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>
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>
* 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>
* 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>
* 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>
#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).
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.
* 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>
* 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>
_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).
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
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>
* 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>
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>
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>
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>
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>
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>
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>
* 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>
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>
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>
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>
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.
* 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>
* 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>
* 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>
* 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>
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.
* feat(dashboard): add RADAR_ENABLED flag (default off)
* feat(db): radar feed cache + settings with encrypted supporter key
* feat(radar): signed feed sync with pinned key and version floor
- feedSchema.ts: Zod v4 schema mirroring the server feed format
(discriminated union on budget.kind, enum constraints, etc.)
- pinnedKeys.ts: Ed25519 SPKI-DER pinned key + env override for forks
- verify.ts: signature verification over exact wire bytes, never throws
- sync.ts: full download/verify/validate/cache pipeline with injectable
deps, feature-flag gate, opt-in gate, version floor (numeric compare),
and sanitized error reasons (no stack traces)
- 40 tests covering: contract hash, key handling, sig verification,
schema validation, version compare, all sync paths (disabled, opt_out,
invalid_signature, invalid_schema, stale, updated, error), auth header
injection, and cache-untouched assertions for every failure mode
* feat(radar): read-time overlay merge rules over the free catalog
Pure function applyFeed() merges the cached Radar feed over the static
baseline catalog at read time, honoring 4 rules:
1. Feed never overwrites a local override field.
2. enabled:false disables the entry with disabledBy:"radar" provenance.
3. User-added entry NOT in the feed survives untouched.
4. User deletion tombstone prevents feed resurrection.
getRadarCatalog() accessor in index.ts: flag off / no cache / corrupt
payload all fall back to baseline. Valid cache applies the overlay and
returns feed metadata (version, tier, fetchedAt).
TDD: 19 tests (4 rules + dedup + origin + accessor flag/cache/corrupt/
valid/bad-feed + baselineToMergedEntries converter).
* feat(dashboard): radar catalog and guided setup screens
- API routes: GET /api/radar/catalog, POST /api/radar/sync, POST /api/radar/settings
- All gated on RADAR_ENABLED flag (404 when off)
- Error responses via buildErrorBody(), never raw stack/message
- Settings never echoes clear supporter key (masked omr_****<last4>)
- Sync delegates to syncRadar() server-side, never proxies feed URL
- Dashboard pages:
- /dashboard/radar: 4 states (flag off, opt-in pending, empty, populated)
- /dashboard/radar/setup?provider=X: guided setup with steps, key URL, test connection
- Uses existing Card component and next-intl patterns
- Sidebar: radar entry in costs group with icon
- i18n: pt-BR and en keys for radarPage and radarSetupPage namespaces
- Tests:
- radar-api-routes.test.ts: 11 tests (flag-off 404, flag-on shape, error sanitization)
- radar-page-state.test.ts: 5 tests (pure state logic)
- All 90 radar tests pass (including prior 74)
* docs(radar): module doc and flag-off inertia test
Add docs/frameworks/RADAR.md covering the flag gate, the separate data-sync
opt-in and privacy promise, the Ed25519 signature/pinned-key security model,
tiers, the read-time overlay merge rules, and the self-hosting env vars —
plus index entries in CLAUDE.md/AGENTS.md/docs/README.md/REPOSITORY_MAP.md.
Document RADAR_FEED_URL and RADAR_FEED_PUBKEY in .env.example and
docs/reference/ENVIRONMENT.md to satisfy check:env-doc-sync, which was
failing on this branch since the sync.ts commit added the reads.
Add tests/unit/radar-inertia.test.ts as the single canonical place asserting
the "RADAR_ENABLED off => zero behavioral delta" claim end to end: the three
/api/radar/* routes 404, the flag resolves to the definition default with no
override, getRadarCatalog() returns exactly the baseline without touching the
cache, and computeFreeModelTotals() keeps its pinned values with the Radar
module imported alongside it.
* fix(db): renumber radar migration to 135 after collision with 134
The base branch introduced 134_proxy_logs_egress_ip while this branch carried
134_radar_cache_settings; the migration runner rejects duplicate numeric prefixes.
This migration has never been applied to a real database (the PR is unmerged), so
no retroactive isSchemaAlreadyApplied guard is needed.
* i18n(radar): translate radar catalog and setup strings to all locales
The UI-coverage ratchet measures (present - placeholder) / total_en, so the
__MISSING__ sentinels that i18n:sync-ui writes do not count as covered — only
real translations restore the metric. Scoped to this PR's namespaces
(radarPage, radarSetupPage, sidebar.radar*) instead of a bulk sync, which would
have pulled ~978 unrelated pending keys into this diff.
Placeholders and code identifiers verified preserved across all 1682 strings.
* fix(radar): trust the served-tier header instead of the signed body field
The signed feed body always carries tier:"live" by design (one signed
artifact per version — rewriting the field server-side per request
would break the exact-bytes Ed25519 signature). The server now returns
the tier ACTUALLY served via the x-omniroute-feed-tier response
header, so free users on a delayed community snapshot no longer see
"Ao vivo (tempo real)" in the UI.
sync.ts now reads and validates that header (falling back to the
body's tier only when the header is absent or holds an unrecognized
value) and stores the served tier in the cache; index.ts already
surfaces cache.tier to the UI unchanged.
* test(combo): shorten an assert message that exceeded the line limit
The assertion added by #9507 was 104 chars, so prettier reformatted it into
five lines on the next commit that touched the file, pushing it past its
frozen size (3449) and failing check:file-size. The message is shortened
(the issue reference stays in the comment directly above); the assertion
itself is unchanged, and the file is back to 3448 lines and prettier-clean.
* i18n(radar): use the canonical zh-TW glossary terms
The machine translation produced retired renderings the glossary gate blocks:
供應商 for provider (canonical 提供者) and 文檔 for documentation (canonical 文件).
Fixed across the 11 affected radar strings; tests/unit/i18n-glossary-consistency-check.test.ts
is back to 17/17.
* fix(radar): point the default feed URL at the domain that exists
radar.omniroute.dev was a placeholder for a domain that was never registered,
so an out-of-the-box sync would fail DNS resolution for every user. The live
feed is served from radar.omniroute.online (the subdomain the design always
specified), now behind Cloudflare TLS. Forks still override it via
RADAR_FEED_URL.
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
* fix(build): exec native tool binaries directly in runBuildTool
#8858 routed every resolved local bin through process.execPath to avoid
Windows .cmd shims — but esbuild >=0.25 ships bin/esbuild as the NATIVE
platform executable (ELF on Linux), so Node parsed machine code as JS and
build:cli died with 'SyntaxError: Invalid or unexpected token', turning
dast-smoke red for every PR.
runBuildTool now sniffs the entry's magic bytes (ELF / Mach-O / PE) and
execs native binaries directly; JS entries keep going through this Node
binary (the .cmd-shim avoidance #8858 wanted).
Validation (RED->GREEN on this box):
- RED: node node_modules/esbuild/bin/esbuild --version -> SyntaxError (ELF)
- GREEN: the exact failing CI step reproduced via the new logic bundles
open-sse/mcp-server/server.ts successfully (4.2MB output, 1.3s).
* fix(docs): add MDX frontmatter to the 20 remaining docs without it
Same failure class as AGENTROUTER_WAF (#9503) and DOCKER_RELEASE_CHANNELS
(this run's dast-smoke red): any doc without frontmatter breaks the
fumadocs MDX loader during next build, killing build:cli/dast-smoke for
every PR. Swept ALL of docs/ (i18n mirrors excluded) in one pass so this
class cannot recur one file at a time.
* docs(env): document OMNIROUTE_INTERNAL_SERVICE_TOKEN(+_FILE), OPENROUTER_PROVIDER_STATS_* and embedded-Redis binding vars
Pre-existing env/docs contract drift from recently merged features made
check:env-doc-sync red for any docs-touching PR. Values and defaults read
from the defining modules (internalServiceAuth.ts, openrouterProviderStats.ts).
* fix(build): resolve bundled npm-cli.js in the standard Unix layout + safe npm fallback off-Windows
The opencode-plugin step hard-failed on GitHub runners because
resolveBundledNpmEntry only looked next to the node binary (Windows zip
layout); hostedtoolcache Node keeps npm at <prefix>/lib/node_modules/npm.
Added that candidate, and when neither exists on non-Windows the step now
falls back to plain 'npm' — the .cmd-shim hazard #8858 avoids is
Windows-only.
* test(mutation): register xai-agent-tools-passthrough.test.ts in stryker tap.testFiles
The test landed on release/v3.8.50 covering
open-sse/handlers/chatCore/passthroughHelpers.ts without the stryker
registration, so Fast Quality Gates' drift detection reds any PR that
carries it. Mechanical registration so its mutant kills count.
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
* fix(mcp): give the audit tests a loader seam createRequire cannot hide from
Since #8959 the audit DB loads better-sqlite3 via createRequire() (so
Electron/global-install resolution works) — which vi.doMock cannot
intercept: it only patches Vitest's ESM module graph. The audit.test.ts
better-sqlite3 mock therefore never engaged; the tests opened a REAL
empty sqlite file in the temp DATA_DIR ('no such table: mcp_tool_audit'
on stderr) and every mock assertion counted zero calls. The 3 failures
are deterministic (reproduced 3/3 locally), redding Vitest (fast-path)
for the entire PR queue — long misdiagnosed as a flake (#9095 merge
notes call it 'pre-existing audit.test.ts flake').
- Shutdown tests inject the mock through the audit connection cache
(globalThis.__omnirouteMcpAuditDb) — the module's own seam.
- The node:sqlite fallback test drives __setBetterSqliteLoaderForTests,
a test-only loader override; the production createRequire path is
untouched (node:sqlite itself is import()'d, so its doMock still
works).
3/3 red -> 3/3 green; full open-sse/mcp-server vitest suite 88/88.
* chore: align changelog slug with the PR number (9559)
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
* fix(quality): reconcile inherited file-size drift on the release tip
13 files sit above their frozen LOC on the clean tip 8180b49ce1
(measured by the gate itself). The PR-mode base-relative check (#8522)
correctly lets innocent PRs pass, but per-PR rebaselines were lost
across successive conflict resolutions of this hot file during the
08-05/06 merge batch — so the absolute mode (nightly, local runs) is
permanently red and stops distinguishing real growth from inherited
drift.
Frozen values updated to the measured tip, each annotated with the
merged PR that grew the file (#9024#9324#9329#9193#9332#9228#9236#9314#9260#8934#9196#9163); executors default.ts and kiro.ts
(above the 1000 cap with no frozen entry) join the frozen set.
* fix(quality): prune orphaned ESLint suppressions and clear the 5 unsuppressed errors
The 'No new ESLint warnings' job reds the whole queue with exit 2:
'There are suppressions left that do not occur anymore' — the 08-05
merge batch removed code whose violations were frozen in
eslint-suppressions.json, leaving orphaned entries (673->670 files,
4338->4333 violations after eslint --prune-suppressions).
The full-tree run also surfaced 5 real unsuppressed errors merged with
the batch, fixed here instead of suppressed (new violations must be
fixed, per policy): 4x no-explicit-any in
tests/unit/catalog-order-contract.test.ts ((conn as any).id -> typed
cast) and 1x react/no-unescaped-entities in the agent-bridge
SetupWizard (#9095).
Also restores the _comment policy header the successive hot-file
conflict resolutions had dropped (TS7 debt freeze provenance + prune
policy).
* fix(quality): absorb the two file-size growths merged while this PR was in CI
The base kept moving during the reconcile cycle: #9184 grew
src/sse/handlers/chat.ts 1857->1877 and #9005 grew
open-sse/executors/default.ts 1027->1042. Re-measured on the merged
tree; gate back to 0 violations.
* fix(tests): move the orphaned RTL ratchet test to a collected path as node:test
#8828 added tests/unit/scripts/check-rtl-ratchet.test.ts — a path no
runner collects (the node:test globs enumerate an explicit subdir list
without scripts/, and vitest.config.ts never included it), so the file
NEVER ran and the test-discovery orphan gate reds the queue. Moved to
tests/unit/ (collected by node:test) and converted from vitest
describe/it/expect to node:test+assert to match the runner and the
sibling check-*.test.ts files. 5/5 green under the real runner.
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
* fix(build): resolve npm-cli.js on POSIX layouts in the shim-free prepublish resolver
The #8858 resolver only tried <dir(node)>/node_modules/npm/bin — the
Windows layout. On POSIX (GitHub hosted runners, nvm, system installs)
npm lives at <prefix>/lib/node_modules/npm while node is <prefix>/bin/
node, so resolveBundledNpmEntry returned null and npm run build:cli
died installing @omniroute/opencode-plugin deps on every fresh checkout
('npm-cli.js not found next to the running Node binary') — redding Fast
Production Build and dast-smoke for the whole PR queue.
Extract the resolver to scripts/build/resolveNpmEntry.ts with injectable
seams and try, in order: npm_execpath (exported by npm run itself), the
Windows beside-the-binary layout, the POSIX <prefix>/lib layout.
TDD: tests/unit/build/resolve-npm-entry.test.ts — the POSIX-layout and
npm_execpath cases plus a live regression guard fail against the old
single-candidate logic (2/5) and pass with the fix (5/5).
* docs(env): register the 7 env vars orphaned by the 08-05 merge batch
The Docs Gates env/docs contract went red on the release tip: #9260
added OMNIROUTE_INTERNAL_SERVICE_TOKEN(_FILE) and #9324 added
OPENROUTER_PROVIDER_STATS_ENABLED/_TTL_MS without .env.example entries,
and the #9286 Redis sidecar vars (REDIS_BIND_HOST, REDIS_PORT,
OMNIROUTE_REDIS_BIND_HOST) never reached ENVIRONMENT.md. Inherited
base-red on every open PR. Defaults and descriptions taken from the
consuming source files.
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
* fix(quality): resolve net-new lint errors and allowlist #9343 assert rewrite
Two `no-explicit-any` errors landed with #9407 and #9320 after the
suppressions inventory was generated. Project policy is to fix new
violations rather than freeze them, so both are typed instead:
- #9407: `executor as unknown as Record<string, unknown>`
- #9320: `(k: { name?: string })`
Also allowlists the net-assert reduction in web-tools-translation-2820
(39->35). #9343 inverted the contract — bare JSON must no longer be
promoted to tool_calls without an explicit <tool> envelope — so the
tests were rewritten to assert non-promotion, which costs fewer asserts
than validating a promoted object. More restrictive, not weaker.
* fix(quality): raise integration ceiling to 40min and unpin codex-cli version in test
The integration gate's 20min ceiling killed a healthy run: measured 22m08s
hermetic on an idle 16-core box (935 tests across 112 files, strictly serial
at --test-concurrency=1 because ~16 of them bind a port or share a DB). The
"~3-10min" estimate in the code was stale by ~3x. 40min keeps the ceiling's
real purpose — turning a genuine hang into a visible failure — without
failing a long-but-healthy suite.
Also fixes a base-red in chat-pipeline: 564c204efe bumped
DEFAULT_CODEX_CLIENT_VERSION to 0.146.0 but the User-Agent assertion still
pinned 0.144.1. The line two above already read the constant via
getCodexClientVersion(); this one duplicated the literal. Deriving it from
the same source stops the next bump from breaking the test again.
* fix(ratelimit): re-arm Bottleneck reservoir heartbeat after updateSettings
Bottleneck 2.19.5 (frozen upstream dependency, no release since 2019) has a
bug in LocalDatastore#_startHeartbeat() (node_modules/bottleneck/lib/
LocalDatastore.js:29,56): the guard `if (this.heartbeat == null && ...)`
only (re)creates the periodic reservoir-refresh interval the first time it
runs. Every later call -- including the one updateSettings() itself
triggers internally -- falls into the else branch and does
clearInterval(this.heartbeat) WITHOUT resetting the reference back to
null. Because the stale reference sticks around, every future
_startHeartbeat() call keeps taking the same dead else branch: the
periodic reservoir refresh is gone forever after the first manual
updateSettings() call on a limiter.
Every limiter created by this file starts with a live heartbeat
(buildLimiterDefaults() always sets reservoirRefreshInterval/
reservoirRefreshAmount), so the very first updateFromHeaders() /
updateFromResponseBody() / applyRequestQueueSettings() call against a
limiter permanently kills its refresh. In production this wedges the
request queue once the reservoir hits 0: an auto-enrolled apikey
connection accumulates its default 60 requests, the reservoir zeroes, the
queue freezes for ~120s, the watchdog fires a synthetic 502
(RATE_LIMIT_QUEUE_WEDGED), the connection cools down and gets excluded
from weighted combo pools -- turning a configured 70/30 split into
~50/50.
Add applyLimiterSettings(), a module-local wrapper around
limiter.updateSettings() that nulls the stale heartbeat reference and
re-invokes _startHeartbeat() afterward so it takes the "start a fresh
interval" branch again. Route all 5 updateSettings() call sites through
it (updateAllLimiterSettings, both updateFromHeaders() branches,
loadPersistedLimits(), and updateFromResponseBody()). updateAllLimiterSettings
is now async and awaited by its two callers (initializeRateLimits,
applyRequestQueueSettings); the sync call sites use the existing
trackAsyncOperation() fire-and-forget tracking pattern.
tests/integration/combo-matrix/weighted.test.ts is the E2E proof: the
"weighted: 70/30" case now passes with zero WEDGED/RATE_LIMIT_QUEUE/502
log lines across 200 sequential requests (previously the wedge/recovery
cycle inflated its runtime and skewed the distribution toward ~50/50).
Refs #8213
* fix(tests): remove stray TDD probes committed by accident in f4e93f339d
Three TDD repro/probe test files landed on the release tip via
f4e93f339d (docs: add management authentication terminology guide,
files from a worktree. Each file is a pre-fix TDD probe that belongs
to a *different*, still-in-flight fix branch/PR and duplicates a file
path that PR already owns and will properly update on merge:
- tests/unit/authz/probe-9033-repro.test.ts: probe for #9033 (IP
blacklist direct-connection bypass). 3/4 asserts fail against this
tree (D1, D2, Bonus — all assert the not-yet-implemented target
behavior); D3 passes (pre-existing behavior). Owned by PR #9385
(open, unmerged), which modifies this exact path.
- tests/unit/repro-8522.test.ts: probe for #8522 (absolute file-size
baseline reds innocent PRs on inherited drift). First test fails
against this tree's evaluateFileSizes (still absolute-only); second
(sanity: real growth still flags) passes. #8522 is actually CLOSED
upstream — PR #9355 merged the real fix into release/v3.8.50 today
(2026-08-05T15:53Z) modifying this exact path — but this branch's
merge-base with release/v3.8.50 (6b0e11e378) predates that merge,
so the fix has not synced into this tree yet.
- tests/unit/repro-8956.test.ts: probe for #8956 (resolveProjectRoot
stops at synthetic Next.js standalone package.json). First test
fails against this tree; second (sanity: named package.json still
resolves) passes. Owned by PR #9354 (open, unmerged), which
modifies this exact path.
Each deleted file's real implementation + passing version already
exists in its owning PR and will land normally through that PR's own
merge — deleting the premature copy here does not lose any coverage.
No config/quality/test-masking-allowlist.json entry was added: the
_deletedWithReplacement schema only supports `replacement` (a test
file that must already exist in this tree's HEAD — none does, the
real versions live in the unmerged sibling PRs above) or `sourceRemoved`
(production files that must be absent from HEAD — they are not, none
of the three issues are implemented in this tree). Neither shape fits
an "owned by an in-flight sibling PR" deletion, so the CI test-masking
gate will flag these 3 deletions for mandatory human review on this
branch's next PR diff against release/v3.8.50 — flagged for the owner
rather than inventing a new allowlist shape.
Refs #9033, #8522, #8956, #7786
* fix(tests): align 8189-classifier-compat with #9276 always-mode semantics
tests/unit/8189-classifier-compat-auto-narrow.test.ts was a test-sibling
forgotten when #9276 (commit 6b531fbacd) removed the unconditional
`if (mode === "always") return true` branch from
shouldDefaultAllowClassifier(). tests/unit/claude-classifier-compat.test.ts
was updated in that same commit; this file was not.
Old contract: 'always' mode short-circuited every Claude-format request
unconditionally (operator opt-in was treated as sufficient on its own).
New contract: 'always' now requires the same SECURITY_MONITOR_MARKER
system-prompt text as 'auto' — the marker-optional behavior let a normal
chat request through /v1/messages be silently swallowed by an operator's
'always' opt-in.
The single 'always' test (1 assert, no-marker body expecting true) is
replaced by two tests mirroring the depth already used for 'auto' mode
in the same file: no-marker/false and marker-present/true. Net effect is
+1 assert, not a reduction — the new pair verifies both directions of
the narrowed contract instead of only the now-incorrect unconditional
case.
Before: 3/4 pass (the 'always' test failed: expected true, got false).
After: 5/5 pass.
Refs #9276
* fix(tests): align deepseek-web-tools-execute with #9343 tool envelope contract
tests/unit/deepseek-web-tools-execute-2820.test.ts (executor level) was a
test-sibling forgotten when #9343 (commit d969555417) hardened tool-call
parsing: bare JSON with no explicit <tool>/<tool_call> envelope is never
promoted to tool_calls anymore (previously it was, whenever a tools[] set
was requested — a security gap allowing prose/code-fenced JSON echoed
back by the model, or a copy-attack, to trigger real tool execution).
Three siblings were updated in the same commit: web-tools-translation.test.ts
and web-tools-translation-2820.test.ts (parseToolCallsFromText, the shared
translator), and deepseek-web-tools-variants.test.ts (parseDeepSeekToolCalls,
deepseek-specific parser) — all inverted their bare-JSON assertions to
`toolCalls === null` + `content === text` (preserved verbatim, not stripped).
This file calls the executor's execute() (full HTTP round trip through
buildToolAwareResult), so it was not touched by that diff and kept
asserting the old contract (finish_reason: "tool_calls", content: null).
Verified against source (open-sse/executors/deepseek-web.ts
buildToolAwareResult): when parseDeepSeekToolCalls returns toolCalls=null,
hasCalls is false, so finish_reason is "stop", message.tool_calls is never
set, and message.content is the parser's returned content — which for text
with no <tool>/<tool_call> tag at all is the original string, unchanged
(parseToolCallsFromText's early-return branch). The test now asserts
exactly that shape, at the same executor level as the rest of the file's
tool_calls that make sense at that level as the rest of the file's tool_calls
Refs #9343
* fix(tests): align visionBridge tests with #8430 contract (partial — see note)
Two test-siblings were forgotten when #8430 (commit 7e55abbc41) hardened
Vision Bridge's vision-model selection: getBestVisionModel() now validates
that a candidate has a usable active connection (hasUsableCredentialsForModel,
DB-backed) before returning it, instead of unconditionally returning the
fixedModel or a hardcoded "openai/gpt-4o-mini" default. Three siblings were
updated in the same commit (visionBridgeRouter.test.ts, the new
repro-8430.test.ts, vision-bridge-preserve-on-failure-4012.test.ts); these two
were not.
tests/unit/guardrails/visionBridgeHelpers.callVisionModel.test.ts (8 failures,
all "No vision-capable provider connected"): callVisionModel()'s `routerConfig`
param only merges into getBestVisionModel's CONFIG argument, never its `deps`
argument, so there is no way to inject a credentials stub through this
function's public signature (unlike the guardrail class and getBestVisionModel
itself, which do accept an injectable `hasUsableCredentials`). These tests
exercise callVisionModel's own request/response handling, not credential
routing (already covered elsewhere), so the fix seeds one real usable
`provider_connections` row per provider the file exercises (openai, anthropic)
via createProviderConnection in a test.before() hook, with resetDbInstance()
in test.after() per the DB-handle-cleanup convention. All 8 now pass.
tests/unit/guardrails/visionBridge.test.ts (7 failures): 1 of the 7 (VB-S03)
is a genuine forgotten-contract case, fixed here — same semantic flip already
applied to vision-bridge-preserve-on-failure-4012.test.ts: in the combo
describe path, when EVERY describe call fails, the raw image is now replaced
with an "(unavailable)" stub instead of preserved, because that path is only
reached for confirmed non-vision targets. Assertions inverted to match
(imagePart undefined, unavailable-stub present), same assert count, no
weakening.
*** THE OTHER 6 (VB-S12, VB-S12b, VB-S01, VB-S13, VB-S07, VB-S10) ARE
DELIBERATELY LEFT FAILING. *** These are NOT a #8430 contract change — root-
caused to what looks like a separate, unintentional regression: the ONE call
to getBestVisionModel() in visionBridge.ts's whole-request-reroute path
(line 244, `getBestVisionModel({ fixedModel: configuredModel })`) does not
pass a `deps` second argument, so it always uses the real DB-backed
hasUsableCredentialsForModel instead of this.deps.hasUsableCredentials — even
though the two adjacent checks in the very same function (`checkCreds(model)`
at line 226, `checkCreds(bestModel)` at line 246) DO honor the injectable
override. In this suite's empty-but-readable isolated test DB, that real
check deterministically returns `false` (not the indeterminate `null` the
file's own createGuardrail() comment says these tests rely on: "Fail-open
(null) so classic VB-S01/S07/S10 reroute tests keep working without a live
credential DB"), so getBestVisionModel silently returns null, the reroute
branch's `if (bestModel && ...)` guard never fires, and every test that
expects a reroute observes a silent no-op instead.
Evidence this is a source gap, not a test that needs updating:
- The file's own pre-existing comment names VB-S01/S07/S10 as tests the
`null` fail-open default is SUPPOSED to keep green.
- VB-CRED-01/02 (the file's only two tests that actually inject a non-default
hasUsableCredentials mock) both pass today, but neither one's assertions
distinguish "mock honored" from "mock ignored, real check also says no" —
they don't prove the threading works, they just don't happen to notice it's
missing.
- visionBridgeRouter.test.ts, repro-8430.test.ts, and
vision-bridge-preserve-on-failure-4012.test.ts (22 tests, all green) all
either call getBestVisionModel directly with explicit deps, or mock
callVisionModel wholesale (bypassing getBestVisionModel entirely) — none of
them exercises this exact call site through the guardrail's own deps.
Per instructions, this was intentionally NOT "fixed" by weakening these 6
tests' assertions (that would mask the gap) or by seeding fake DB credentials
to route around it (that would hide a real production DI inconsistency behind
a test-only workaround) or by touching src/lib/guardrails/visionBridge.ts
(a production behavior change outside a test-alignment task's scope, and
Hard Rule #18 requires its own TDD/validation cycle). Flagging for the owner:
the likely one-line fix is threading `{ hasUsableCredentials:
this.deps.hasUsableCredentials }` as getBestVisionModel's second argument at
visionBridge.ts:244, mirroring the two adjacent call sites in the same
function.
Before: 15 failures (7 + 8). After: 9 pass added (1 + 8), 6 still fail
(unchanged, by design).
Refs #8430
* feat(quality): add strayFromCommit deletion allowlist form to test-masking gate
The deletion allowlist supported two shapes: replacement (test rewritten
elsewhere) and sourceRemoved (feature deleted). Neither fits a third
legitimate case surfaced today: test files that entered the repo BY
ACCIDENT — commit f4e93f339d (#7786 docs) swept another session's
worktree artifacts into the release, including TDD probes owned by open
fix PRs (probe-9033-repro -> PR #9385, repro-8956 -> PR #9354,
repro-8522 -> PR #9355). Those probes fail by design until their owning
PR merges, so every unit run on the release tip broke on them.
The new strayFromCommit form is verified, not trusted: the gate asks git
which commit actually ADDED the file (git log --diff-filter=A) and only
exempts the deletion when it matches the declared hash; a non-empty
reason naming the owning PR/issue is mandatory. Also allowlists the
deepseek-web-tools-execute assert reduction (23->21) from ed661f2126 —
same #9343 contract-inversion class as the existing
web-tools-translation entry.
Gate unit tests: 55/55 pass. Full gate vs main: OK.
* fix(guardrails): pass credential deps to getBestVisionModel at reroute call site
The individual-model reroute path in VisionBridgeGuardrail.preCall() calls
getBestVisionModel({ fixedModel: configuredModel }) without its second
`deps` argument, so the router always falls back to the real DB-backed
hasUsableCredentialsForModel instead of an injected
`deps.hasUsableCredentials` override. The two adjacent credential checks in
the same function (the original-model check and the best-model check,
both via the local `checkCreds` binding) already thread deps correctly —
only this middle call, added in #8430, was left out.
Pass the same resolved `checkCreds` used by those two adjacent checks as
`getBestVisionModel`'s deps argument so all three credential checks in this
reroute path stay consistent.
Fixes 6 tests in tests/unit/guardrails/visionBridge.test.ts that depended
on the injected hasUsableCredentials mock being honored on this path:
VB-S12, VB-S12b, VB-S01, VB-S13, VB-S07, VB-S10.
Refs #8430
* fix(quality): raise unit ceiling to 100min and align 2 more forgotten sibling tests
Unit ceiling 45->100min: a hermetic-env measurement on the loaded devbox
(load 7-26) was still inside invocation 1 of 3 at 76min when killed;
contention factor 2-3x measured, no idle measurement exists. The
pre-flight's real condition is exactly that contended one (unit runs in
Promise.all with integration+vitest), and there 45min provably killed a
healthy suite and fabricated a false base-red. The 45min value came from
v3.8.43 as an estimate never validated by measurement. TODO in-code:
re-tighten after an idle run on the .113 box.
Also aligns the 6th and 7th occurrences of the same systemic pattern
(behavior change merged updating only part of the sibling tests):
- issue-7859-gemini-web-redirect-valid: #9407 refined ServiceLogin
redirects to mean expired session; the #7859 regression coverage is
preserved via a non-ServiceLogin public redirect variant.
- provider-validation-specialty claude-web 429: #9406 inverted the
contract (rate-limited session is unhealthy); the dedicated repro file
owns the full contract, this sibling now matches it.
Also carries the file-size rebaseline for #9323's base.ts growth
(1578->1623, WAF retry + burst guard) and the eslintWarnings baseline
tightened 5000->0 (real measured value with the TS7 suppressions in
place — 5000 left the ratchet inert).
Refs #9407, #9406, #9323
* fix(tests): restore the 3 TDD probes now owned by merged fixes and drop their stray allowlist entries
The base advanced while this PR was open: the real fixes for the three
issues behind the stray probes all merged into release/v3.8.50 —
#9385 (issue 9033), #9355 (issue 8522) and #9354 (issue 8956).
- probe-9033-repro / repro-8522: the base rewrote both probes into the
regression tests of their merged fixes, so the delete side of the
rebase conflict was dropped and the base versions kept.
- repro-8956: #9354 only realigned one fixture line in
auto-update.test.ts (package.json marker now needs a name field) and
added no test for the new skip-synthetic behavior — the probe is the
ONLY regression coverage of that merged fix (2/2 green on the base),
so deleting it would remove real coverage. Restored.
With no test-file deletions left in the PR diff, the three
strayFromCommit allowlist entries are stale and removed. The
strayFromCommit form support in check-test-masking.mjs stays (covered
by its own fixtures).
* fix(quality): rebaseline file-size for PR #9529 own growth
The base sits exactly at the old frozen values, so the base-relative
mode (#8522) does not cover this growth — it is this PR's own:
- open-sse/services/rateLimitManager.ts 1060->1105: the
applyLimiterSettings() helper that re-arms the reservoir heartbeat
after updateSettings (Bottleneck 2.19.5 fix, TDD in
ratelimit-reservoir-refresh.test.ts).
- tests/integration/chat-pipeline.test.ts 1592->1598: codex User-Agent
derived from getCodexClientVersion() instead of a pinned literal.
- tests/unit/provider-validation-specialty.test.ts 2980->2985: new
claude-web 429 -> valid:false coverage (#9406).
* fix(docs): sync provider count to 291 in README and CLAUDE
The live catalog counts 291 providers but README.md/CLAUDE.md still
said 290, so the STRICT 'Docs Gates (fast-path)' check reds EVERY open
PR against release/v3.8.50 (verified on #9537/#9539 as well — inherited
base-red, not introduced by this PR). Updated all provider-count
mentions including the section anchor.
* fix(tests): align launch-codex 6312 guard with the async #9454 spawn contract
#9454 made resolveCodexSpawn async (PATH-probes a native codex.exe before
the .cmd shim) and updated its own tests, but left this older sibling
calling the function synchronously — destructuring the Promise yields
undefined and reds Unit fast-path (1/4) for EVERY open PR against the
release (verified on #9537/#9539; inherited base-red). Realigned to the
async contract with an injected probe; keeps the original #6312 fallback
guard plus the only non-Windows codex coverage (now also asserting the
probe never runs off Windows).
* fix(translator): move state-mutating reasoning summary helper out of the pure leaf
#9500 added buildResponsesReasoningSummaryDelta(state, ...) to
pureHelpers.ts, but the function reads AND mutates stream state
(reasoningSummaryIndex map) — violating the leaf contract declared in
the file header ('no host imports, no stream state') and guarded by
response-openai-responses-purehelpers-split.test.ts, which reds Unit
fast-path (4/4) for every open PR (inherited base-red, verified on
#9537/#9539). Moved verbatim to the host next to the other stream-state
helpers (markResponsesReasoningDeltaEmitted); the host was its only
consumer. Behavior unchanged: repro-9500-reasoning-separator 3/3 green,
leaf/host architecture tests green.
* fix(quality): rebaseline openai-responses.ts for the leaf-state relocation
The #9500 helper moved from pureHelpers.ts into the host (previous
commit) grows the host file 1174->1204 while the leaf shrinks by the
same amount — net-zero LOC across the pair, but the per-file frozen
ratchet only sees the growing side.
* fix(tests): let the 9442 cert-mode test see past the harness trust-store guard
tests/_setup/isolateDataDir.ts sets OMNIROUTE_SKIP_SYSTEM_TRUST=1
globally, which makes installCert() return before issuing any command —
so the #9442 install-gap test captured nothing and could NEVER pass
under npm run test:unit (it only passed invoked directly, harness-less;
inherited base-red on Unit fast-path 3/4, verified on #9537/#9539).
Clear the flag for this file only (restored in test.after): safe because
every spawned command is a logging stub on PATH and OMNIROUTE_NO_SUDO=1
strips sudo, so nothing touches the real trust store. 6/6 under the CI
harness including system-trust-test-guard.
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
* docs: centralize agent instructions in AGENTS.md; CLAUDE/GEMINI point to it
- AGENTS.md becomes the single source of truth: full CLAUDE.md content (main
data) merged with the AGENTS.md-only sections (documentation accuracy,
repository map, review focus, upstream contributions) and the GEMINI.md-only
file-placement/root-hygiene and local-access rules. Adds the base-green
check section (PRs must not be born red) and fixes the stale
_tasks/release-flow path in Hard Rule 21.
- CLAUDE.md: @AGENTS.md pointer + Claude-Code-only operational deltas
(EnterWorktree, subagent stash-ban replication, superpowers path overrides,
base-green/sweep-reds pointers).
- GEMINI.md: pointer + Gemini-only notes; the stale 10-item hard-rule mirror
is removed (source is the 22-rule list in AGENTS.md).
- ci(release-green): label the not-green tracking issue with base-red.
* docs: retarget docs-sync provider/MCP claims to AGENTS.md and fix test placeholder
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
The #7786 squash accidentally committed its worktree copy
(.claude/worktrees/feat-7786/**, since untracked) and leaked probe tests
(repro-8522/probe-9033/repro-8956 — each now green via #9355/#9385/#9354)
plus a stray changelog.d/fixes/9159-fix.plan.md describing an UNMERGED fix
(would fabricate a changelog entry at release time — removed; #9159's own
PR ships its fragment).
This restores the PR's actual deliverable at the right paths: the
management-auth terminology guide (now with the required MDX frontmatter),
its docs test (3/3 green) and its changelog fragment.
A Next.js standalone build writes a synthetic .build/next/package.json
({"type":"commonjs"}) that lacks a "name" field. The resolveProjectRoot()
walk-up was stopping at this marker instead of continuing to the real repo
root, making PROJECT_ROOT point at .build/next where no .git exists, which
caused the source-mode validation to report "Not a git repository."
Fix: only accept a package.json as a project-root marker when its parsed
content has a non-empty "name" field. Keep .git as a hard marker.
Add isValidPackageMarker() helper for testability.
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
The 2026-08-05 TS7 rebaseline wrote 5000 measured WITHOUT the suppressions
file, but the PR gate (quality.yml lint:json + quality:collect) measures WITH
suppressions applied and reads 0 - so require-tighten failed every code PR
with delta 5000 > slack. Measured 0 on the pure tip ed122b2caf after the
stale-suppression prune (#9509). TS7 debt remains tracked in
config/quality/eslint-suppressions.json; any NEW warning outside it is an
immediate red, which is the policy.
release/v3.8.50 fails its own "No new ESLint warnings" gate right now,
independent of what any PR changes. Measured directly: a worktree
checked out at the current tip alone, no PR merged in, exits 2 with
"There are suppressions left that do not occur anymore." Cross-checked
against two unrelated open PRs (#9499, #9497) hitting the identical
failure, ruling out anything content-specific.
The mass-freeze commit that regenerated config/quality/eslint-suppressions.json
for the TypeScript 7 migration left one entry pointing at a violation
that no longer exists: src/lib/usage/providerLimits.ts no longer
triggers no-restricted-imports, but the suppression entry for it does.
ESLint's own suppression bookkeeping treats an unmatched entry as a
hard failure, separate from and in addition to real unsuppressed
errors.
--prune-suppressions removes exactly that one entry. It also drops the
informal "_comment" key documenting the freeze's origin, since ESLint's
suppression writer only round-trips file-keyed entries it manages
itself -- that context is not lost, it is still readable at the
mass-freeze commit (6b0e11e37) in git history.
This is one of two independent problems behind the same gate failure,
not the whole fix. Two files (tests/unit/issue-9407-gemini-web-validation-false-positive.test.ts,
tests/unit/v1-models-auth-leak-9320.test.ts) carry real, currently
unsuppressed no-explicit-any errors with no entry covering them at
all -- pruning cannot add what was never there. #9484 fixes those at
the source. Verified here that after this change alone, the gate
moves from exit 2 (stale suppressions) to the ordinary exit 1 those
two remaining errors cause -- both this and #9484 need to land before
the gate is green again.
Signed-off-by: Minxi Hou <houminxi@gmail.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Missing title/version/lastUpdated frontmatter broke the production build
(fumadocs-mdx requires title on every docs/**/*.md file).
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
* fix(tests): clear the two base-reds on release/v3.8.50
Both sat on the release itself and turned every open PR red as soon as it
merged the release, independently of the PR's own content.
- tests/snapshots/provider/translate-path.json: #9064 (b0501642dd) added the
code-execution-2025-08-25 and skills-2025-10-02 beta flags to the Anthropic
header but did not regenerate the golden. provider-translate-path-golden
failed on the bare release tip — 2 pass / 1 fail with zero PRs boarded.
Regenerated; the diff is 24 lines, all the same header in 6 variants.
- tests/unit/v1-models-auth-leak-9320.test.ts:83: shipped a (k: any) in a file
new enough that eslint-suppressions.json does not cover it. With
@typescript-eslint/no-explicit-any as error under tests/, that one cast
failed 'No new ESLint warnings' for every PR. The callback parameter infers
correctly, so the cast was redundant.
Verified on the fix branch: eslint exit 0, typecheck:core exit 0, and both
test files green (5/5).
* fix(tests): drop a third unsuppressed any (gemini-web validation test)
A full-repo lint on this branch surfaced one more file in the same class:
tests/unit/issue-9407-gemini-web-validation-false-positive.test.ts:50 casts
(executor as any).testConnection. The file entered the release at f1ea77fd04
(23:28), newer than the frozen eslint-suppressions.json, so the cast is not
covered — and it alone kept 'No new ESLint warnings' red on this very PR.
testConnection is a declared public method on GeminiWebExecutor
(open-sse/executors/gemini-web.ts:359), so the cast was redundant rather than
load-bearing; removed outright.
eslint exit 0, typecheck:core exit 0, 15/15 across the three touched tests.
- Updated the quality baseline to set eslintWarnings value to 5000, reflecting the migration to TypeScript 7 and the new warning thresholds.
- Modified the test masking allowlist to account for removed tests and sources, ensuring proper tracking of deprecated features.
- Enhanced ESLint configuration to ignore additional directories containing non-source files.
- Removed the .npmignore file as its contents are now managed in package.json.
- Adjusted KimiWeb model configuration to correctly map K3 to the K2D5 scenario, reflecting changes in the underlying logic.
- Updated artifact packing policy to prevent nested node_modules from being published, ensuring a leaner package size.
- Added tests to verify the exclusion of node_modules from published artifacts and to ensure the integrity of the package.json files array.
Acquire admission once after API-key policy, preserve lazy raw-request snapshots, and bind lease settlement to JSON, SSE, abort, deadline, and failure lifecycles. Expose a low-cardinality health summary and preserve non-SSE Ollama errors unchanged.
Add bounded weighted admission with fair queuing, deadline and cancellation handling, exact lease accounting, and a default-shadow runtime. Keep asynchronous resource-pressure shedding as an independent safety fuse and bound request feature estimation.
Return a typed HTTP 504 for OmniRoute's per-target timer, keep fallback active, and classify the local timeout as request-scoped so it cannot degrade provider connection health.
Bumps ip-address, hono, fast-uri, socket.io-parser, undici (v6+v7),
protobufjs, and tar via targeted package.json overrides.
All patches are lockfile-only (no code change, range already covers).
Verified: npm audit → 0 vulnerabilities.
Note: brace-expansion NOT in overrides (separate major lines need
different patches; each resolved within its parent range).
Co-authored-by: wgordon17 <22222756+wgordon17@users.noreply.github.com>
* chore(ci): stop dependabot from grouping ioredis majors with routine bumps
ioredis is loaded through a dynamic import in the distributed quota store, so a
breaking major passes build, typecheck and both test suites and only surfaces at
runtime for operators running Redis-backed quota. #9310 grouped ioredis 5.10.1 to
6.0.0 with 9 unrelated production bumps; majors get their own PR from now on.
* docs(changelog): add fragment for #9425
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
* fix(resilience): count STREAM_EARLY_EOF as a provider failure in combo routing
A STREAM_EARLY_EOF is an upstream that accepted the request (HTTP 200), opened
the SSE stream, then closed it without emitting a single non-ping event. The
combo path classified it together with STREAM_READINESS_TIMEOUT through
isStreamReadinessFailureErrorBody(), and the readiness exemption in
shouldRecordProviderBreakerFailure meant the whole-provider circuit breaker
never saw it.
During a provider-wide outage that makes the breaker blind. Over a 7-day window
on our router we recorded 311 of these events, 302 of them on one model, 265
inside the upstream's published incident window — and the provider breaker sat
at CLOSED / failure_count=0 the entire time. Every request kept being dispatched
to the failing provider instead of shedding to the next combo target.
The two codes are different signals. The readiness probe is a pre-flight
liveness check on a connection we have not committed to, so failing it means
"this connection looks stale". An early EOF means the provider took the request
and then failed to serve it. The single-model path already treats it that way:
shouldTripProviderBreakerForResult has no readiness exemption, so a 502 early
EOF trips the breaker there. This makes the combo path consistent.
isStreamReadinessFailureErrorBody keeps matching both codes, because the
transient-retry and round-robin semaphore-cooldown paths in combo.ts do want
identical treatment for both. Only the breaker needs to tell them apart, so the
distinction is added as a narrow predicate and an optional argument rather than
by changing the shared classifier. Omitting the new argument reproduces the
previous behaviour exactly.
Follows the additive-override pattern established by the isProxyUnreachable
work, and leaves the existing exclusions for client aborts and plain 429s
untouched.
* test: register stream-early-eof-breaker in stryker tap.testFiles
The mutation test-coverage gate (check:mutation-test-coverage --strict)
detects unit tests that cover a mutated module but are missing from
stryker.conf.json tap.testFiles, so their mutant kills would not count.
comboPredicates.ts is one of the mutated modules, and the new
stream-early-eof-breaker.test.ts covers it, so the gate correctly flagged
the omission. 8376-econnrefused-breaker.test.ts -- the test this one is
modeled on -- is already registered; this just brings the new file in line.
No production code change.
---------
Co-authored-by: Nick Sullivan <nick@technick.ai>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
The local vi.mock("next-intl") predates the #7935 global polyfill and returns a
useTranslations without .rich, crashing t.rich() in ProviderParamFilterSection:199.
The global polyfill (backed by the real createTranslator) now covers this file;
assertions only check DOM/fetch, never translated text.
Co-authored-by: Max <maxmad64@gmail.com>
* fix(dashboard): add per-connection autoSync toggle handler
* fix(dashboard): render per-connection autoSync toggle in ConnectionRow
* fix(dashboard): wire canAutoSync into ConnectionsListPanel
* fix(dashboard): wire per-connection autoSync toggle into provider page
* fix(dashboard): make master autoSync toggle all-on with fan-out
* docs(dashboard): add changelog fragment for per-connection autoSync
* fix(dashboard): correct disable toast and assert fan-out classification
* test(dashboard): pin fan-out classification branches symmetrically
* docs(dashboard): fill changelog fragment with PR number
* fix(dashboard): port autoSync i18n keys to vi and pt-BR locales
* fix(dashboard): localize autoSync keys across all 43 locales
---------
Co-authored-by: Max <maxmad64@gmail.com>
#9275 started appending a candidate-alias hint to the zero-active-credentials
error and terminated the provider name with a period, so the two sentences read
as one message. The two vscode tokenized-route tests still assert the old
unterminated string and now fail on every pull request opened against this
branch.
The Quality Gates workflow only runs on pull_request to release/**, never on
push, so the branch itself never re-runs these shards and the drift stayed
invisible after the merge.
Assert what the handler actually produces. Keeping the comparison exact rather
than loosening it to a prefix match is deliberate -- the exact form is what
caught the drift.
Signed-off-by: Minxi Hou <houminxi@gmail.com>
* fix(routing): only let Codex-native bare ids preempt a provider when codex is active
#9275 widened CODEX_NATIVE_UNPREFIXED_MODELS from a single id to gpt-5.5 plus the
gpt-5.6-sol/terra/luna tiers, so bare Codex CLI ids would reach the ChatGPT
subscription instead of fanning out to whichever provider won the inference race.
The early return it added never consulted the active-provider set, which made the
codex-only guard 30 lines below unreachable for every id in the set:
if (CODEX_NATIVE_UNPREFIXED_MODELS.has(modelId)) return { provider: "codex", ... }
An OpenAI-only install therefore had bare gpt-5.5 routed to codex and failed with
'no active credentials for provider: codex' on a model OpenAI serves, and an install
whose codex connection was merely inactive failed identically. This also silently
reverted #5887's compatibility boundary.
The preference now only PREEMPTS another provider when a codex connection is active.
Ids that no other provider catalogs (codex-auto-review) still resolve to codex with no
connection at all — there is nothing to preempt and 'no codex credentials' is the
honest error. With codex active the preference still beats OpenAI, which is the point
of #9275, and an explicit openai/ prefix overrides it either way.
Tests: the three assertions that encode the intended #9275 change now expect codex
(plus a new one pinning the explicit-prefix override); the rest were already correct
and pass again untouched. Adds a regression test for the OpenAI-only case.
* docs(changelog): correct fragment id to #9447
* test(routing): seed an active codex connection in the bare-precedence guards
The two files #9275 added assert that bare gpt-5.5 / gpt-5.6-sol reach codex, but
they ran against an empty database — so they also pinned 'codex wins with no codex
connection at all', which is the regression #9447 removes. That put them in direct
contradiction with plan3-p0 / chat-helpers / codex-gpt55-routing-5887, which assert
openai for the very same input: no implementation could satisfy both, which is why
the release could not go green.
Seeding an active codex connection keeps the contract these files were written to
guard (codex beats openai for a Codex-native bare id) while dropping the accidental
'even with no codex configured' half. Cases that need no connection are left as they
were: the tier-only ids and codex-auto-review have no alternative provider to preempt,
and the explicit-prefix overrides are unaffected.
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
* feat(providers): filter detail connections server-side
Filter provider detail requests at the database boundary while preserving
the full per-provider connection set needed by search, pagination, and bulk
actions. Alias-backed provider pages keep their existing aggregate behavior.
Co-authored-by: RobertsXML <RobertsXML@proton.me>
Inspired-by: https://github.com/decolua/9router/pull/2998
* chore(changelog): fragment for #9247
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Co-authored-by: RobertsXML <RobertsXML@proton.me>
* fix(db): persist account egress IP into proxy_logs
The account egress IP (outbound IP the upstream saw, resolved via proxyEgress.ts
echo-IP probe with 5-min cache) was computed and surfaced in the proxy_logs
console and ring buffer, but never persisted: proxy_logs.egress_ip did not
exist, so the value was lost on restart and real traffic could not be
attributed to the node/IP active at that instant.
- migration 134 adds proxy_logs.egress_ip (nullable, backward-compatible)
- schemaColumns.ensureProxyLogsColumns() idempotent reconciler
- proxyLogger self-heals the schema in loadFromDb(), persists egress_ip on
INSERT, and matches it in search
Follows the session_tag (#8249) migration + schemaColumns reconciler pattern;
base SCHEMA_SQL untouched.
* docs(changelog): add 9291 fragment for proxy_logs egress_ip
---------
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Root cause: a cold GET /v1/models catalog rebuild froze the entire server 41-54s.
node --prof profiling found a systemic missing-memoization pattern — a per-model
function rescanning a static or synced data structure with Object.entries()/
Object.keys() (or hitting SQLite) on every call instead of once per rebuild. Fixed
6 instances of the same pattern, found by iteratively re-profiling the full catalog
sweep after each fix (plus a whitebox review pass) until no further hotspot of this
shape remained:
1. getModelsDevPricing() (modelsDevSync.ts) — re-ran a synchronous SQLite query and
re-JSON.parse'd ~180 blobs on every call (up to ~6091x instead of once per
request). Memoized via the existing modelCatalogCacheVersion invalidation signal
(same pattern as getCachedRawProviderConnections/getCachedProviderNodes in
db/readCache.ts). Dominant cost of the original 41-54s freeze.
2. findInsensitive() (modelMetadataRegistry.ts, resolveCatalogPricing) — rebuilt a
full Object.entries() scan on every case-insensitive lookup miss, twice per
model. Replaced with a lowercase-key index built once per distinct pricing
object and cached by identity (WeakMap). Warns once at index-build time on a
case-insensitive key collision instead of silently discarding the second value.
3. getSyncedCapability() (modelsDevSync.ts) — ran a per-model SQLite SELECT on cold
cache instead of self-warming the whole-table cache; no caller in the
/v1/models build path ever primed it, so a cold rebuild ran one SQLite
round-trip per model per call site. Now self-warms via the existing bulk
getSyncedCapabilities() on first miss. Measured as the dominant remaining cost
after fixes 1-2 (~70% of a full catalog sweep).
4. getCanonicalModelSpecId() (shared/constants/modelSpecs.ts) — up to 3 separate
linear scans over the static MODEL_SPECS table per call (exact ci, alias ci,
prefix). Replaced with a lazy, lowercase-key index built once (MODEL_SPECS never
changes at runtime); prefix-match iteration order preserved exactly so
resolution outcomes are unchanged.
5. getStaticSpecCanonicalModelId() (modelCapabilities.ts) — duplicated the same
exact+alias scan as (4) in a second, separate rescan. Now reuses the shared
index via a new exported helper (findModelSpecIdByExactOrAlias) instead of
maintaining a second cache over the same static table.
reverseModelsDevProviders() (modelCapabilities.ts) — rescanned
Object.entries(MODELS_DEV_PROVIDER_MAP) (also static) on every call; memoized
by provider key. Result is frozen (readonly) since it is now shared across
calls instead of freshly allocated each time.
6. resolveModelAlias() (shared/constants/modelSpecs.ts) — rescanned
Object.entries(MODEL_SPECS) unconditionally once per model (verified 1:1 call
ratio, no short-circuit). Case-sensitive exact match (Array.includes(), no
.toLowerCase()) — uses a dedicated exact-match index, deliberately not the
case-insensitive alias index from fix 4/5 (would silently broaden matches).
Measured on a 1940-pair real-catalog sample (static PROVIDER_MODELS registry):
cold sweep 828ms -> 356ms after fixes 3-5 on top of 1-2, extrapolating to roughly
1s on the real ~6091-model catalog, down from the original 41-54s freeze.
Complementary to the stale-serve fix in #8801 (upstream) — neither alone
eliminates the freeze.
Tests: call-count regression guards for every fix (DB prepare / Object.entries /
Object.keys call counts staying constant instead of scaling with iteration count),
plus correctness coverage for case-insensitive/case-sensitive resolution. All
pre-existing consumer suites re-verified passing (96 tests total across 19 files).
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
* fix(token-refresh): exempt transient errors from exponential backoff
A refresh that failed on a network timeout was treated exactly like one
that failed on a revoked token: the streak incremented and the circuit
backed off exponentially, up to four hours. A brief upstream blip could
therefore park a healthy account for the rest of the day.
Transient failures now take a flat two-minute retry window instead of
advancing the streak. Classification checks structured signals first
(err.name for AbortError/TimeoutError, then err.code and err.cause.code)
and only falls back to matching the message text, so it does not depend
on upstream wording. Everything else keeps the existing exponential path.
Two properties worth preserving on sight:
- A transient failure never shortens a longer permanent backoff. The
new window is only adopted when the existing one is not already
further out.
- testStatus is preserved on both paths, so a connection whose access
token is still valid keeps serving requests while its refresh
retries.
Only a successful refresh clears the circuit. A successful request does
not, because requests do not refresh tokens.
* chore(quality): rebaseline file-size for tokenHealthCheck.ts
src/lib/tokenHealthCheck.ts lands at 1021 lines, above the 1000 cap. The
file consolidates token-refresh health checking that was previously split
across auth.ts and tokenRefresh.ts, and the refresh circuit state machine
does not divide cleanly, so splitting it to satisfy the cap would cost
more than it buys.
Scoped to this file only. Baseline entries for files this branch does not
touch are left at their upstream values.
Image and video payloads vary by provider and base64 encoding adds substantial overhead. Exempt media routes from OmniRoute's global request-body cap so provider-specific validation determines whether a request is too large. Keep finite body limits for non-media routes and cover both header and streamed-body admission paths.
The claude->openai translator was unconditionally rewriting max to xhigh, which broke any OpenAI-shape upstream that accepts max literally (e.g. ollama-cloud, opencode-go deepseek, moonshot k3, native Claude). Provider-aware effort policy is owned by sanitizeReasoningEffortForProvider in the executor; the translator should only do form conversion.
Regression guard: tests/unit/base-executor-sanitize-effort.test.ts end-to-end case (claude -> ollama-cloud preserves max).
Modal-hosted OpenAI-compatible endpoints (self-hosted Kimi K3 via
Modal free tier) return HTTP 429 with body {"error":"usage limit
reached"} when the account's credit is exhausted. Previously no
QUOTA_PATTERNS regex matched this bare-string error shape, so the 429
fell through to rate_limit (60s short cooldown). Combined with combo
round-robin's per-conversation session stickiness (#3825), this kept
re-targeting the same exhausted connection every turn instead of
locking it out and failing over to an account with remaining credit.
Add a substring pattern matching the JSON key/value pair
"error":"usage limit reached" with tolerance for trailing
punctuation and whitespace. Only the exact "error" key matches;
different keys or qualified transient messages like "Per-minute usage
limit reached" stay classified as rate_limit.
Signed-off-by: Minxi Hou <houminxi@gmail.com>
getCombos() SELECT was missing the id column, so returned combo objects
had their id come only from the JSON data blob. If the data blob lacked
an id field, callers (including the Dashboard) saw null — making the
combo appear to have no primary key and impossible to delete.
Add id to the SELECT so the database column value is always available.
Signed-off-by: Minxi Hou <houminxi@gmail.com>
* fix(ci): restores dast-smoke queue tolerance, drops batching
PR #7329 (an unrelated cliproxy feature PR) silently reverted two prior
Mergify fixes when it touched .mergify.yml from a stale branch:
- #7225's tolerance for the advisory dast-smoke check, which hangs
recurrently on GitHub-hosted runners (issue #7226) and had been
dequeuing every queue attempt it touched.
- #7220's removal of batch_size/batch_max_wait_time, which is a paid
Mergify tier feature this repo's free plan does not have (the queue
command fails outright with it set).
Restores both fixes verbatim. No PR has used the queue label since
#7329 landed two weeks ago, so this had gone unnoticed.
* fix(ci): restores the auto-enqueue merge_protections_settings block
The first pass of this fix missed a second piece #7329 clobbered in
the same diff hunk: merge_protections_settings.auto_merge_conditions,
the actual mechanism that puts a queue-labeled PR into the queue (the
older rules-based autoqueue path it replaced is EOL). Without it, the
queue label was a no-op even after restoring the check-failure
tolerance and dropping batching.
.mergify.yml now matches commit 9875ccf4e (the last known-good state
before #7329) byte-for-byte, confirmed via sha256.
* fix(ci): retargets queue tolerance from dast-smoke to Build (advisory)
Evidence review found the prior fix's dast-smoke exception is stale:
dast-smoke has failed only twice ever, none since 2026-07-13 (0/30 in
the last ~3.3h across many PRs). Meanwhile Build (advisory), added to
quality.yml 2026-07-27, has a 100% failure rate on every sampled PR
since — confirmed via job logs to be the same class of runner hang
(dies mid "Creating an optimized production build", never a real
compile error), just in a check dast-smoke's tolerance never covered.
Retargets the merge_conditions exception accordingly so the queue can
actually tolerate the failure mode it faces today, instead of one
that's been dormant for weeks.
The agentrouter.org upstream WAF returns 400 content-blocked
intermittently when:
1. messages[].content contains a blocked keyword (Lorem ipsum, the
phrase 'language model' alone, 'virtual assistant', etc.); or
2. Requests from the same IP/key arrive in a burst, after which the
WAF's per-IP suspicion bucket starts blocking content that would
normally pass. The bucket relaxes after ~5-10s of idle.
Apply three mitigations:
1. Burst guard (open-sse/services/wafRateLimit.ts)
Per-bucket (provider+url) gate that enforces a 500ms minimum gap
between outbound requests to agentrouter. Configurable via
configureWafRateLimit(). Tested in tests/unit/wafRateLimit.test.ts.
2. Reactive retry (BaseExecutor.WAF_RETRY_CONFIG in base.ts)
New WAF_RETRY_CONFIG with maxAttempts=2, delayMs=1500,
backoffMultiplier=2. When the upstream returns 400 with a body that
matches /content[_-]blocked/i, retry the same URL with exponential
backoff (1.5s, 3.0s) before falling through to the 429/401/fallback
chain. Tested in tests/unit/base-executor-waf-retry.test.ts.
3. Documentation (docs/security/AGENTROUTER_WAF.md)
Blocklist of always-blocked and almost-always-blocked patterns,
behavior under load, guidance for prompts/tool output, and pointers
to the relevant code paths in OmniRoute.
These are belt-and-suspenders: the burst guard prevents the WAF from
activating on normal traffic, and the reactive retry recovers when it
does anyway. Together they should eliminate the intermittent
400 content-blocked that Claude Code sees when running through
agentrouter via OmniRoute.
Refs #9275 follow-up. Test: 'WAF retry config shape' and 'WAF retry
differs from generic' guard the WAF_RETRY_CONFIG contract so future
refactors don't accidentally collapse the two retry paths.
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
* fix(routing): bare model ids route to codex first; validate synced candidates
Two bare-model-routing bugs surfaced in the field when an OmniRoute
deployment had a codex subscription whose cookie quota was exhausted
(retry-after 429047s / ~5 days) AND an active kiro connection whose
upstream sync briefly advertised 'claude-opus-5' before kiro vendored
it into the static registry.
1. Bare 'gpt-5.6-sol' (and friends) routed to the codex provider even
when the user had explicitly configured 'agentrouter' as their
provider (via model_provider in codex CLI). With codex in cooldown,
every bare request 429'd. Fix: extend CODEX_NATIVE_UNPREFIXED_MODELS
to include the full gpt-5.6-sol tier set + gpt-5.5 + the related
codex-native ids. The Codex CLI default is now actually honored;
users can still prefix 'agentrouter/gpt-5.6-sol' to opt into a
specific provider.
2. Bare 'claude-opus-5' silently routed to 'kiro' when kiro's synced
/v1/models catalog had that id (likely from a transient upstream
quirk). kiro's static registry never cataloged claude-opus-5, so
the upstream call 404'd. Fix: validate activeSyncedProviders against
MODEL_TO_PROVIDERS before merging them into the candidate list.
Auto-discovery still wins when the model id has no static entry
(brand-new models from upstream keep working).
Bonus: when handleNoCredentials returns a 404 'No active credentials for
provider: X' error, surface the top-3 candidate aliases (e.g.
'anthropic/claude-opus-5, claude/claude-opus-5, agentrouter/claude-opus-5')
so the operator can pick a working prefix instead of staring at a wall.
Tests (all pass, 25 regression tests preserved):
- tests/unit/fix-bare-model-precedence.test.ts (7 tests)
- tests/unit/fix-synced-model-validation.test.ts (3 tests)
- tests/unit/fix-error-message-candidates.test.ts (3 tests)
- tests/unit/fix-bare-routing-fallback.test.ts (7 tests)
* fix(tests): replace lorem ipsum with neutral text to avoid agentrouter WAF
The agentrouter.org WAF blocks requests containing 'lorem ipsum' in
messages[].content. When Claude Code reads test files via the Read tool,
the content appears in tool_result blocks which can trigger the filter.
Replace 'lorem ipsum dolor sit amet' with 'example content for testing
purposes' in compression harness test to avoid false positives.
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
The worktree-isolation recipe in `CLAUDE.md` told agents to symlink `node_modules` from the main checkout. That silently breaks the dev server.
Turbopack refuses a symlink that resolves outside the project root, so `npm run dev` dies with a FATAL panic while typecheck, lint and both test runners keep passing — the message names "filesystem root", not the worktree, so it reads like a Next/build problem.
`cp -al` gives the same benefit (no per-worktree npm install) without the defect: ~5s for the 4.4 GB tree and near-zero extra disk. Verified on this very worktree: same inode, link count 2.
Mirrored into the two translated CLAUDE.md copies that carry the command (zh-CN translated, pl left in English to match its surrounding section).
Adds the `?utm_source=omniroute` tracking parameter to every public-facing URL where Cheaper Inference is clickable.
- README: the two `<a href>` targets in the Open Source Friends table row (logo + "Get an API key" CTA)
- `gateways.ts`: the `website` field and the `apiHint` text
Not changed on purpose: `api.cheaperinference.com/*` endpoints (technical, not clicks), JSDoc mentions (descriptive text), and the `<sub>cheaperinference.com</sub>` label under the logo (plain text, not a link).
fix(agentrouter): infer protocol from client endpoint
- /v1/responses resolves AgentRouter as openai-responses
- /v1/chat/completions resolves AgentRouter as openai
- /v1/messages resolves AgentRouter as claude
- Per-request protocol and credential cloning (no SQLite mutation)
- Codex 0.146.0 and Claude Code 2.1.220 identity alignment
- response.completed.usage.total_tokens normalization for strict Codex clients
Closes#9224
* docs(readme): add Affiliates Promo section (coupon for AgentRouter)
New collapsible <details> block under '🤝 Supported by our Open Source Friends',
labeld 'Affiliates Promo' to keep it visually and editorially separate from
actual sponsors. Sized at ~half (icon 32px, sub-tag text) so it does not
compete with the partner block above.
First entry: AgentRouter — $100 signup credit (per FREE_TIERS.md), free server
with higher latency, first-class support since v3.8.50. Models surfaced:
claude-opus-4-8, claude-opus-5, gpt-5.6-sol — with a live-list link to
agentrouter.org/v1/models so users can verify.
Clear caveat: 'Affiliate link — OmniRoute has no sponsorship or partnership
with this provider.' — and a footer inviting more coupons via issue.
* docs(readme): leave Affiliates Promo expanded by default
The block only has a single entry today; collapsing it would hide the
AgentRouter coupon from a casual skim. Add 'open' to <details> so the
content is visible on first load. Users can still collapse it manually.
* docs(readme): drop 'see live list at agentrouter.org/v1/models' sentence
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
- npm run test:scoped: runs only tests impacted by your changes
- npm run test:scoped:staged: for staged changes (pre-commit)
- Uses select-impacted-tests.mjs with impact map when available
- Falls back to heuristic (changed test files) when no map
- Hub file changes suggest full suite
- 7 unit tests for the selection logic
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
G1 (v3.8.51): section (2) of check-known-symbols no longer regex-scans
strategy === "..." literals from combo source. The handled set now comes
from a runtime-imported dispatch registry (open-sse/services/combo/
strategyDispatch.ts) that imports the real ordering functions and enumerates
which strategies they implement. This keeps the canonical-not-handled gate
correct under the upcoming R0.3 registry dispatch, which removes the
strategy === branches the regex relied on.
- Adds HANDLED_COMBO_STRATEGIES registry (all 20 canonical strategies) + binds
the real dispatch leaves (applyStrategyOrdering, resolveAutoStrategyOrder,
tryFusionDispatch, tryPipelineDispatch, resolveComboTargetPipeline).
- main() imports the registry instead of reading/sourcing combo files.
- extractHandledStrategies + diffComboStrategies stay exported (pure, tested).
- New TDD test proves the runtime enumeration covers canonical exactly.
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
* feat(ci): G0 — quality rail (PR→release/**) ganha ratchets+segurança do trilho A
O refactor de god-files do trilho 3.8.50→3.9.0 acontece em PRs→release/**, e esse
trilho pulava o motor de ratchet, o CodeQL ratchet e todos os scanners de segurança
— exatamente onde a rede era necessária (5 das 13 causas da reconciliação de 07-24
eram regressões reais shipadas por CI verde por-PR).
Modo enxuto, jobs EXISTENTES (a .51 consolida lanes; nenhum job novo):
- lint-guard: quality:collect + ratchet --allow-missing + require-tighten +
check:codeql-ratchet. O job já escreve .artifacts/eslint-results.json, então o
motor entra a custo ZERO de ESLint (um inventário, dois consumidores). Coverage
ausente degrada gracioso (--allow-missing); autoridade de coverage segue no
trilho A. + permissions security-events:read para o CodeQL ratchet.
- fast-gates: check:cycles, check:lockfile, duplication, dead-code, type-coverage,
compression-budget + install endurecido dos scanners (gh release download,
zizmor PINADO 1.25.2 = mesmo auditor do ci.yml) + secrets/vuln/workflows/
openapi-breaking com --ratchet (self-skip sem binário; só regressão medida bloqueia).
- Fora de propósito: bundle-size (self-skip sem build → configuração morta) e o
run de coverage (fast-unit já roda a suíte cheia).
Runners intocados: guard tests/unit/vps-runner-variable-scope.test.ts verde;
teste novo tests/unit/quality-rail-gate-membership.test.ts pina a MEMBERSHIP dos
gates no trilho B (red antes da edição, green depois).
Validação no tip puro (nenhum base-red fabricado para a fila de PRs abertos):
cycles OK · lockfile OK · duplication 4.26% (base 5.72%) · dead-code 226 (base
227) · type-coverage 94.13% (base 92.17%) · compression OK · secrets 0 (base 0) ·
vuln 5 (base 10) · codeql 0 (base 0) · oasdiff 0 (base 0) · zizmor 178 (base 190)
· actionlint exit 0 no arquivo editado · quality-ratchet 56 métricas OK +
require-tighten OK com --allow-missing.
Refs #8084
* feat(.50): G13 golden-set, G14 import boundaries, gap34 deterministic, docs sync, R0.2 dead hooks
Integra os itens restantes da 3.8.50:
- G13: golden-set determinístico para combo.ts e chatCore.ts via seams públicas
- G14: no-restricted-imports para localDb barrel fora de src/lib/db/ e executors em src/app/
- Gap34: teste determinístico de timeout DuckDuckGo sem rede real
- Docs: golden path de contribuição + sincronização de números canônicos
- R0.2: remoção dos 7 hooks mortos do BUILTIN_EVENTS + UI marketplace ajustada
* fix(r0.2): remove marketplace tab remnants from plugins page — fixes dashboard typecheck regression
* chore(r0.2): remove pluginWorker.ts, signing.ts, sandbox.ts — zero importers confirmed
* fix(docs): remove OMNIROUTE_PLUGINS_ALLOW_EXEC reference — env var removed with pluginWorker.ts in R0.2
* fix(env): remove dead OMNIROUTE_PLUGINS_ALLOW_EXEC from .env.example — consumer removed in R0.2
* fix(test): update sidebar-visibility assertion for R0.2 marketplace removal
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
O refactor de god-files do trilho 3.8.50→3.9.0 acontece em PRs→release/**, e esse
trilho pulava o motor de ratchet, o CodeQL ratchet e todos os scanners de segurança
— exatamente onde a rede era necessária (5 das 13 causas da reconciliação de 07-24
eram regressões reais shipadas por CI verde por-PR).
Modo enxuto, jobs EXISTENTES (a .51 consolida lanes; nenhum job novo):
- lint-guard: quality:collect + ratchet --allow-missing + require-tighten +
check:codeql-ratchet. O job já escreve .artifacts/eslint-results.json, então o
motor entra a custo ZERO de ESLint (um inventário, dois consumidores). Coverage
ausente degrada gracioso (--allow-missing); autoridade de coverage segue no
trilho A. + permissions security-events:read para o CodeQL ratchet.
- fast-gates: check:cycles, check:lockfile, duplication, dead-code, type-coverage,
compression-budget + install endurecido dos scanners (gh release download,
zizmor PINADO 1.25.2 = mesmo auditor do ci.yml) + secrets/vuln/workflows/
openapi-breaking com --ratchet (self-skip sem binário; só regressão medida bloqueia).
- Fora de propósito: bundle-size (self-skip sem build → configuração morta) e o
run de coverage (fast-unit já roda a suíte cheia).
Runners intocados: guard tests/unit/vps-runner-variable-scope.test.ts verde;
teste novo tests/unit/quality-rail-gate-membership.test.ts pina a MEMBERSHIP dos
gates no trilho B (red antes da edição, green depois).
Validação no tip puro (nenhum base-red fabricado para a fila de PRs abertos):
cycles OK · lockfile OK · duplication 4.26% (base 5.72%) · dead-code 226 (base
227) · type-coverage 94.13% (base 92.17%) · compression OK · secrets 0 (base 0) ·
vuln 5 (base 10) · codeql 0 (base 0) · oasdiff 0 (base 0) · zizmor 178 (base 190)
· actionlint exit 0 no arquivo editado · quality-ratchet 56 métricas OK +
require-tighten OK com --allow-missing.
Refs #8084
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Registers Cheaper Inference (api.cheaperinference.com) as an OSS-sponsor gateway provider.
- Canonical provider `cheaperinference` (alias `cinf`) + routing registry with 39 measured text models
- Dedicated executor: forces `store:false` on the native /v1/responses endpoint (the shared strip in
chatCore.ts deletes `store` for every provider != openai, so without this every Responses request
400'd) and resolves chat-vs-responses URL from the per-model targetFormat
- 3 image models (grok-imagine, nano-banana-pro, nano-banana-2), prefix-only: the two nano-banana ids
already belong to adobe-firefly, which keeps the bare-id routing
- Resale pricing measured from GET /v1/models (30% off list); sponsor rail Kimi 1st / Cheaper
Inference 2nd via an explicit rank map; supporter badge in 43 locales; README row
No quota card: the gateway exposes no balance API (/v1/wallet and /v1/balance both 404).
Validated live end-to-end through OmniRoute: chat, native Responses, streaming and image generation
all 200 with real content; the Firefly collision guard verified at runtime.
Seed a connection-scoped Gemini thought signature before exercising the Claude-to-Gemini id assertions. This keeps the regression focused on the public-Gemini versus Vertex id contract while honoring the new signature replay behavior.
Constraint: Signature-less historical Claude tool calls are intentionally converted to context text.
Rejected: Emit unsigned functionCall parts in the fixture | Gemini 3+ rejects that production behavior.
Confidence: high
Scope-risk: narrow
Tested: Prettier, repository ESLint hook, vertex-functioncall-id-3440 Node test, git diff --check
Not-tested: Full repository test suite
Direct Claude↔Gemini translator was missing the thought_signature round-trip
that the OpenAI-hub path already had (#2504). Gemini 3+ thinking models strictly
validate thought_signature on every functionCall part in a multi-turn tool-call
batch and return HTTP 400 ("Function call is missing a thought_signature in
functionCall parts") when it is absent — breaking all agentic workflows through
Claude Code.
Three fixes across the direct path:
1. translator/index.ts — thread the per-connection signature namespace
(connectionId) into the direct-path credentials, mirroring the hub path.
Without this, claudeToGeminiRequest never receives _signatureNamespace and
cannot look up stored signatures.
2. translator/response/gemini-to-claude.ts — capture thoughtSignature from
Gemini response parts (functionCall, thought, or standalone signature parts)
via state.pendingThoughtSignature, and persist it keyed by
buildGeminiThoughtSignatureKey(connectionId, toolId). The signature
frequently lands on a preceding thought part rather than the functionCall
itself, so pending tracking across stream chunks is required.
3. translator/request/claude-to-gemini.ts — for each tool_use block, resolve the
stored signature and attach it as thoughtSignature on the functionCall part.
When no signature is available (historical tool calls predating the store, or
cold-start), omit the functionCall part entirely and convert the matching
tool_result to plain text — mirrors openai→gemini context mode, avoiding the
bare-functionCall 400 while preserving conversation context.
2026-07-27 14:25:16 +03:00
1669 changed files with 131983 additions and 28091 deletions
# 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:
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.
| 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` |
@@ -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.
→ If Responses API: responsesTransformer.ts TransformStream
```
API routes follow a consistent pattern: `Route → CORS preflight → Zod body validation → Optional auth (extractApiKey/isValidApiKey) → API key policy enforcement → Handler delegation (open-sse)`. No global Next.js middleware — interception is route-specific.
**Combo routing** (`open-sse/services/combo.ts`): 18 strategies (priority, weighted, fill-first, round-robin, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline). Each target calls `handleSingleModel()` which wraps `handleChatCore()` with per-target error handling and circuit breaker checks. The `fusion` strategy is the exception: it fans out to a panel of models in parallel, then a judge model synthesizes one final answer (`open-sse/services/fusion.ts`). See `docs/routing/AUTO-COMBO.md` for the 12-factor Auto-Combo scoring + the full strategy table and `docs/architecture/RESILIENCE_GUIDE.md` for the 3 resilience layers.
---
## Resilience Runtime State
OmniRoute has three related but distinct temporary-failure mechanisms. Keep their
scope separate when debugging routing behavior. See the
- **ESLint**: `no-eval`, `no-implied-eval`, `no-new-func` = error everywhere; `no-explicit-any` = **error** in `open-sse/` and `tests/` (since #6218 — pre-existing violations are frozen in `config/quality/eslint-suppressions.json`, new ones must be fixed; `npm run lint` applies the suppressions and is what CI runs)
- **Always** go through `src/lib/db/` domain modules — **never** write raw SQL in routes or handlers
- **Never** add logic to `src/lib/localDb.ts` (re-export layer only)
- **Never** barrel-import from `localDb.ts` — import specific `db/` modules instead
- DB singleton: `getDbInstance()` from `src/lib/db/core.ts` (WAL journaling)
- Migrations: `src/lib/db/migrations/` — versioned SQL files, idempotent, run in transactions
### Error Handling
- try/catch with specific error types, log with pino context
- Never swallow errors in SSE streams — use abort signals for cleanup
- Return proper HTTP status codes (4xx/5xx)
### Security
- **Never** use `eval()`, `new Function()`, or implied eval
- Validate all inputs with Zod schemas
- Encrypt credentials at rest (AES-256-GCM)
- Upstream header denylist: `src/shared/constants/upstreamHeaders.ts` — keep sanitize, Zod schemas, and unit tests aligned when editing
- **Public upstream credentials** (Gemini/Antigravity/Windsurf-style OAuth client_id/secret + Firebase Web keys extracted from public CLIs): **MUST** be embedded via `resolvePublicCred()` from `open-sse/utils/publicCreds.ts` — **never** as string literals. See `docs/security/PUBLIC_CREDS.md` for the mandatory pattern.
- **Error responses** (HTTP / SSE / executor / MCP handler): **MUST** route through `buildErrorBody()` or `sanitizeErrorMessage()` from `open-sse/utils/error.ts` — **never** put raw `err.stack` or `err.message` in a response body. See `docs/security/ERROR_SANITIZATION.md`.
- **Shell commands built from variables**: when calling `exec()`/`spawn()` with a script that needs runtime values, pass them via the `env` option (shell-escaped automatically) — **never** string-interpolate untrusted/external paths into the script body. Reference: `src/mitm/cert/install.ts::updateNssDatabases`.
- **Secure-by-default libraries** ([tldrsec/awesome-secure-defaults](https://github.com/tldrsec/awesome-secure-defaults)): prefer Helmet.js, DOMPurify, ssrf-req-filter, safe-regex, Google Tink over custom implementations whenever adding new security-sensitive surfaces.
---
## Common Modification Scenarios
### Adding a New Provider
1. Register in `src/shared/constants/providers.ts` (Zod-validated at load)
2. Add executor in `open-sse/executors/` if custom logic needed (extend `BaseExecutor`)
3. Add translator in `open-sse/translator/` if non-OpenAI format
4. Add OAuth config in `src/lib/oauth/constants/oauth.ts` if OAuth-based — if the upstream CLI ships a public client_id/secret, embed via `resolvePublicCred()` (see `docs/security/PUBLIC_CREDS.md`), **never** as a literal
5. Register models in `open-sse/config/providerRegistry.ts`
6. Write tests in `tests/unit/` (include the publicCreds shape assertion if you added a new embedded default)
### Adding a New API Route
1. Create directory under `src/app/api/v1/your-route/`
2. Create `route.ts` with `GET`/`POST` handlers
3. Follow pattern: CORS → Zod body validation → optional auth → handler delegation
4. Handler goes in `open-sse/handlers/` (import from there, not inline)
5. Error responses use `buildErrorBody()` / `errorResponse()` from `open-sse/utils/error.ts` (auto-sanitized — never put `err.stack` or `err.message` raw in the body). See `docs/security/ERROR_SANITIZATION.md`.
6. Add tests — including at least one assertion that error responses do not leak stack traces (`!body.error.message.includes("at /")`)
### Adding a New DB Module
1. Create `src/lib/db/yourModule.ts` — import `getDbInstance` from `./core.ts`
2. Export CRUD functions for your domain table(s)
3. Add migration in `src/lib/db/migrations/` if new tables needed
4. Re-export from `src/lib/localDb.ts` (add to the re-export list only)
5. Write tests
### Adding a New MCP Tool
1. Add tool definition in `open-sse/mcp-server/tools/` with Zod input schema + async handler
2. Register in tool set (wired by `createMcpServer()`)
3. Assign to appropriate scope(s)
4. Write tests (tool invocation logged to `mcp_audit` table)
4. Add OAuth/credentials handling if needed (`src/lib/oauth/providers/`)
5. Tests + document in `docs/frameworks/CLOUD_AGENT.md`
### Adding a New Embedded Service
1. Create installer in `src/lib/services/installers/{name}.ts` modeled on `ninerouter.ts` (use `runNpm` from `installers/utils.ts` — no shell interpolation, hard rule #13).
2. Register the service in `src/lib/services/bootstrap.ts` (add to `SERVICES[]` array and extend `buildSpawnArgsFactory()`).
3. Add a DB seed row for the new service in `src/lib/db/migrations/` (`version_manager` table, `status='not_installed'`, `auto_start=0`).
4. Create 7 API endpoints under `src/app/api/services/{name}/` (`_lib.ts`, `install`, `start`, `stop`, `restart`, `update`, `status`, `auto-start`). All delegate errors through `createErrorResponse()`. The shared `logs` endpoint is already wired via `[name]/logs/route.ts`.
5. Verify `/api/services/` is in `LOCAL_ONLY_API_PREFIXES` in `src/server/authz/routeGuard.ts`; add a test asserting `isLocalOnlyPath()` returns `true` for the new prefix if you add one (hard rule #17).
6. Add a UI tab in `src/app/(dashboard)/dashboard/providers/services/tabs/` reusing `ServiceStatusCard`, `ServiceLifecycleButtons`, `ServiceLogsPanel`.
7. Document in `docs/frameworks/EMBEDDED-SERVICES.md` (update §1 service table + §4 API reference) and `docs/openapi.yaml`.
8. Write tests: unit (`tests/unit/services/`), integration (`tests/integration/services/`, gated by `RUN_SERVICES_INT=1`), and update `docs/ops/RELEASE_CHECKLIST.md` smoke section.
### Adding a New Guardrail / Eval / Skill / Webhook event
**PR rule**: If you change production code in `src/`, `open-sse/`, `electron/`, or `bin/`, you must include or update tests in the same PR.
**Test layer preference**: unit first → integration (multi-module or DB state) → e2e (UI/workflow only). Encode bug reproductions as automated tests before or alongside the fix.
**Both test runners must pass**: `npm run test:unit` (Node native — most tests) AND `npm run test:vitest` (MCP server, autoCombo, cache) cover **non-overlapping files**. Both are wired in CI (jobs `test-unit` and `test-vitest`) and must be green before merging. A PR where only one suite passes may silently ship broken MCP tools or routing regressions.
**Bug fix / issue triage protocol (Hard Rule #18)**: Every fix for a reported issue must be validated by one of the following — no exceptions:
1.**TDD (preferred)** — write a failing test reproducing the bug → fix it → confirm the test passes. The test becomes the permanent regression guard. Touch only the files the test proves need changing; nothing more.
2.**Real-environment test (when TDD is not possible)** — deploy to the production VPS (`root@192.168.0.15`) and run a documented live test. Record the exact command + result in the PR description. Applies to: OAuth upstream flows, Cloudflare/WS upstream behavior, UI-only regressions, hardware-dependent behavior.
3. "It worked locally without a test" does not count. A fix without a test or a VPS validation record is not a fix — it is a guess.
Why this matters: fixing bug A while opening bug B is worse than not fixing at all. The TDD/VPS gate enforces surgical scope — you touch only what the failing test proves is broken. Examples where this paid off: #3090 (claude-web 403), #3113 (WS HTTP fallback), #3052 (heap-guard auto-calibration).
**Copilot coverage policy**: When a PR changes production code and coverage is below 60% (statements/lines/functions/branches), do not just report — add or update tests, rerun the coverage gate, then ask for confirmation. Include commands run, changed test files, and final coverage result in the PR report.
---
## Planning & Research Artifacts (superpowers, deep-research)
`_tasks/` is a **separate, isolated git repository** that is gitignored by the main
repo (`.gitignore` → `_tasks/`). It is the canonical home for working artifacts —
plans, specs/designs, research, hand-offs — so they stay **versioned in their own
repo** instead of polluting the main OmniRoute tree.
**Hard rule — never write superpowers / planning / research output under `docs/` or
the repo root.** The superpowers skills ship with defaults that point at `docs/…`
In Claude Code prefer the native `EnterWorktree` tool (it already creates worktrees under
`.claude/worktrees/`): create the worktree with the command above, then call `EnterWorktree`
with its `path`.
3. **Work, commit, push, open the PR — all from inside the worktree.** Never `git checkout` a
different branch inside a worktree another session might share.
4. **Tear down only your own** worktree + branch when done, from the main checkout:
`git worktree remove .claude/worktrees/<dir>` then `git branch -D <task>`. Never blanket-delete
`fix/*`/`feat/*` — other sessions keep their own; delete only the branches you created, by name.
5. **Never touch another session's worktree, branch, or uncommitted changes.** If `git worktree
list` shows worktrees you didn't create, leave them alone. End every session with the main
checkout back on the branch it started on (the active `release/vX.Y.Z`, never `main`).
---
## Environment
- **Runtime**: Node.js ≥22.0.0 <23 || ≥24.0.0 <27, ES Modules. This is the **only supported** runtime for the published `omniroute` CLI, the server, and the test suites (`node:test` + vitest) — `engines.node` is authoritative and end users never need Bun. A **best-effort `bun:sqlite` compatibility path** exists so a global Bun install (`bun install -g omniroute`) can start without `better-sqlite3` (driver adapter + Bun-aware process spawning); it is **not** a supported runtime — no support guarantees — and every Bun-specific runtime change MUST preserve the Node driver/fallback chain and ship a Bun test (`test:bun:db`) or an explicit reason why the path is Node-only.
- **Bun (build/dev script runner + compatibility smoke only)**: Bun `1.3.14` is pinned as an **exact devDependency** (provisioned through the existing `npm ci` via the lockfile's `@oven/bun-*` platform binaries — no `setup-bun`/ad-hoc install). It is used **only** to execute a small, allow-listed set of TypeScript **gate/generator scripts** (replacing `node --import tsx` for startup speed): the CI checks `check:provider-consistency`, `check:compression-budget`, `check:known-symbols`, and the non-CI `gen:provider-reference`, `bench:compression` — plus the focused `test:bun:db` compatibility smoke suite for the best-effort `bun:sqlite` path. **Do NOT** widen Bun to `npm install`, the build (`build:cli*`), `check:pack-artifact`, the supported published runtime, or the main test runners — those stay on Node. Any new Bun-invoking gate/generator script must be validated byte-identical against its `node --import tsx` output first. After pulling the lockfile change, run `npm install` so `bun` resolves locally (a stale `node_modules` will fail those scripts with `bun: not found`).
`test:vitest:ui` is advisory until UI component tests are triaged.
**Allowlist policy (short form):** Fix the cause; use the allowlist only for pre-existing
violations you cannot fix in the same PR. Add a comment with justification + issue number.
Stale allowlist entries (suppressing a violation that no longer exists) will be caught by
the stale-enforcement added in Fase 6A.3.
---
## Hard Rules
1. Never commit secrets or credentials
2. Never add logic to `localDb.ts`
3. Never use `eval()` / `new Function()` / implied eval
4. Never commit directly to `main`
5. Never write raw SQL in routes — use `src/lib/db/` modules
6. Never silently swallow errors in SSE streams
7. Always validate inputs with Zod schemas
8. Always include tests when changing production code
9. Coverage must not regress below the baseline frozen in `quality-baseline.json` (ratchet); absolute floor is 60% (statements/lines/functions/branches). Update the baseline via `npm run quality:ratchet -- --update` only when coverage genuinely improves. See `docs/architecture/QUALITY_GATES.md`.
10. Never bypass Husky hooks (`--no-verify`, `--no-gpg-sign`) without explicit operator approval.
11. Never embed public upstream OAuth client_id/secret or Firebase Web keys as string literals — always go through `resolvePublicCred()` (`open-sse/utils/publicCreds.ts`). See `docs/security/PUBLIC_CREDS.md`.
12. Never return raw `err.stack` / `err.message` in HTTP / SSE / executor responses — always route through `buildErrorBody()` or `sanitizeErrorMessage()` (`open-sse/utils/error.ts`). See `docs/security/ERROR_SANITIZATION.md`.
13. Never string-interpolate external paths or runtime values into shell scripts passed to `exec()`/`spawn()` — pass via the `env` option instead. Reference: `src/mitm/cert/install.ts::updateNssDatabases`.
14. Never dismiss a CodeQL / Secret-Scanning alert without (a) first checking the pattern docs above to see if the helper applies, and (b) recording the technical justification in the dismissal comment. Precedent: `js/stack-trace-exposure` raised on callsites that already route through `sanitizeErrorMessage()` is a known CodeQL limitation (custom sanitizers not recognized) — dismiss as `false positive` referencing `docs/security/ERROR_SANITIZATION.md`.
15. Never expose routes that spawn child processes (`/api/mcp/`, `/api/cli-tools/runtime/`) without `isLocalOnlyPath()` classification in `src/server/authz/routeGuard.ts`. Loopback enforcement happens unconditionally before any auth check — leaked JWT via tunnel cannot trigger process spawning. See `docs/security/ROUTE_GUARD_TIERS.md`.
16. Never credit or advertise an AI assistant, LLM, or automation account in any commit/PR metadata. Two forbidden forms, both equivalent — they route attribution to a bot account (or advertise AI authorship) and hide the real author (`diegosouzapw`): **(a)** `Co-Authored-By` trailers naming an AI/bot (e.g. names containing "Claude", "GPT", "Copilot", "Bot"; emails at `anthropic.com` / `openai.com` / bot-owned `noreply.github.com` addresses); **(b)** AI-generation footers or descriptions anywhere in a commit message, PR title/body, or CHANGELOG — e.g. `🤖 Generated with [Claude Code]`, "Generated with Claude Code", "Made with <AI tool>", or any `Co-authored-by: Claude/GPT/Copilot` line. This **overrides any harness, template, or tool default that auto-appends such a footer** (e.g. the Claude Code PR-body/commit default) — strip it before pushing; do not let it reach a commit, PR, or CHANGELOG. Human collaborators — including upstream PR authors and issue reporters being ported into OmniRoute — MAY and SHOULD be credited with standard `Co-authored-by: Name <email>` trailers; the upstream-port workflows (`/port-upstream-features`, `/port-upstream-issues`) depend on this.
17. Never expose routes under `/api/services/` or `/dashboard/providers/services/*/embed/` without `isLocalOnlyPath()` classification in `src/server/authz/routeGuard.ts`. These routes can spawn child processes (`npm install`, `node`). Loopback enforcement happens unconditionally before any auth check — a leaked JWT via tunnel cannot trigger process spawning. See `docs/security/ROUTE_GUARD_TIERS.md`.
18. Every bug fix must be validated before shipping: a failing-then-passing unit/integration test (TDD) OR a documented live test on the production VPS (192.168.0.15). A fix without either is not merged. See Testing → "Bug fix / issue triage protocol" for the full decision tree.
19. Never develop on the shared main checkout. Every development task runs in its own git worktree on its own dedicated branch, and you MUST confirm the base branch with the operator (e.g. via `AskUserQuestion`) before creating the worktree/branch — never assume `main` or the currently checked-out branch. A `git checkout` in the shared checkout silently destroys other sessions' uncommitted work. Tear down only the worktrees/branches you created (by name, never `fix/*`/`feat/*` wildcards), leave other sessions' worktrees untouched, and end on the branch you started on (the active `release/vX.Y.Z`, never `main`). See Git Workflow → "Worktree isolation".
20. PII redaction/sanitization is **opt-in — never on by default**. OmniRoute proxies for self-hosted/local LLMs where the operator owns the data, so mutating request/response payloads by default would silently corrupt legitimate traffic. The two data-mutating PII feature flags **MUST** keep `defaultValue: "false"` in `src/shared/constants/featureFlagDefinitions.ts`: `PII_REDACTION_ENABLED` (request-side) and `PII_RESPONSE_SANITIZATION` (response + streaming). All three application points — `src/lib/guardrails/piiMasker.ts` (request guardrail), `src/lib/piiSanitizer.ts` (response), `src/lib/streamingPiiTransform.ts` (SSE) — are gated on these flags; with both off the `pii-masker` guardrail still runs but never mutates payloads (data passes through untouched). Flipping either default to `"true"` requires explicit operator approval. The regression guard is `tests/unit/pii-opt-in-default.test.ts` (asserts both definition defaults + behavioral pass-through). Opt-in is per-operator via env or the settings/DB override (`src/lib/db/featureFlags.ts`), never a silent default. See `docs/security/GUARDRAILS.md`.
21. **Release-freeze — the FROZEN release branch belongs to the release captain; development does NOT stop (parallel-cycle model, 2026-07-04).** `/generate-release` opens a marker issue labeled `release-freeze` at the start of reconciliation (Phase 0a), **immediately cuts the next cycle's branch `release/vX+1` from the frozen tip (Phase 0a.0b — bump + living release PR + re-home of open PRs)**, and closes the freeze once the release PR squash-merges to `main`. Before merging **any** PR, every campaign workflow (`/review-prs`, `/review-group-prs`, `/merge-prs`, `/triage-fix-bugs`, `/implement-fix-bugs`, `/triage-features`, `/implement-features`, `/green-prs`, `/port-upstream-*`) **MUST** check `gh issue list --repo diegosouzapw/OmniRoute --label release-freeze --state open` — if a freeze is active: **NEVER merge into the frozen `release/vX.Y.Z` named in the freeze title**; instead resolve the ACTIVE development branch (the **highest** `release/v*` by semver — normally `release/vX+1`, announced in a freeze-issue comment) and **retarget the PR there** (`gh pr edit <N> --base release/vX+1`, then VERIFY with `gh pr view <N> --json baseRefName` — the edit fails silently) and merge normally. **HOLD only when the highest release/v\* branch IS the frozen one** (the short window before 0a.0b completes, or a pre-parallel-cycle release) — in that case leave the PR ready and open, tell the operator, and resume when the next branch appears or the freeze lifts. The just-shipped fixes reach `release/vX+1` via the Phase 5 sync-back (`scripts/release/sync-next-cycle.mjs`); do not try to sync mid-release. This is a **coordination signal, not a permission lock**: the release captain and the campaign sessions share the `diegosouzapw` identity, so a GitHub branch-protection lock cannot distinguish them — only this honored marker prevents the mid-release commit races that forced full CHANGELOG re-reconciliation in v3.8.40/v3.8.41 (a parallel campaign advanced `release/vX.Y.Z` by 34 commits mid-run). The release captain's own reconciliation/cycle-open pushes are exempt — they _are_ the release. Fixes that must land during a freeze (a homologation finding) follow the post-merge read-only rule: land on `main` first via `fix/release-vX.Y.Z-*`. **⛔ ONLY `/generate-release` may raise a release-freeze, and ONLY at its Phase 0a (start of generating a new version) — lifted at Phase 12c after the squash-merge to `main`.** No campaign, session, or agent may open a `release-freeze` marker at any other time — a freeze is **never** a mid-development coordination tool. If a session ever believes a freeze is genuinely, unavoidably necessary outside the `/generate-release` flow, it **MUST first ask the operator (`diegosouzapw`) in chat, explicitly alert "estou criando um freeze" and get an explicit yes** — never open, extend, or re-open a `release-freeze` autonomously. Conversely, do **not** close/lift an active `/generate-release` freeze to unblock campaign merges: it protects the captain's single clean CI run and auto-lifts at Phase 12c — closing it early re-triggers the exact commit race it prevents. Verify a freeze is legitimate before acting on it: an open `release-freeze` whose title/body references an **OPEN** release PR (`gh pr view <N> --json state`) is the authorized captain freeze — hold, don't touch.
22. **Cross-session safety — this repo is worked by MANY parallel sessions/agents at once; never step on another's in-flight work.** Two absolute bans, both recurring incidents (this rule exists because they keep happening):
- **(a) Never `git stash` / `git stash pop` — ANYWHERE in this repo, including inside an isolated worktree, and including inside any subagent you dispatch.** `git stash` operates on the **shared repository object store**, not the per-worktree working tree — so a stash pushed or popped in one session can silently clobber or resurrect another parallel session's uncommitted changes. This is not hypothetical: 2026-07-02 a `#5923` quotaCache change leaked into the unrelated `#2296` worktree via a global `stash pop`, and the same class reincided through a **subagent**. To compare working changes against a base ref **without** stashing, use `git show <ref>:<path>` or `git diff <ref> -- <path>`; to confirm a typecheck/lint error is pre-existing on the base, inspect the base ref directly (`git show origin/release/vX.Y.Z:<path>`) — never stash your tree away to "get it clean". **Put this ban verbatim in the prompt of every subagent that touches git** (agents don't inherit this file's context — the recurrence was a subagent).
- **(b) Never merge, push, rebase, or force-push a PR / branch / worktree that another session is actively working.** An open PR whose head is a live fix worktree in `.claude/worktrees/` you did **not** create (e.g. `fix-5852`/`fix-5923` carrying fresh commits, even when they share your `diegosouzapw` identity), or any branch another session owns, is **off-limits — HOLD**, and let the owning session merge it. **Before** merging or pushing to any PR you did not create _this_ session, run `git worktree list` to check for a matching in-flight worktree and re-check `gh pr view <N> --json state,headRefOid`. Only the owning session merges its own in-flight PR; mid-flight merges race the owner and re-trigger the exact commit/CHANGELOG races Rule #19 and Rule #21 guard against. (Reinforces Rule #19.)
---
## PII & Stream Sanitization Learnings
### 1. Regex Security (ReDoS)
All regex patterns matching variable-length strings (e.g. IPv6 address, credit cards) must use strictly bounded, non-overlapping sequences (e.g., limit occurrences with bounded ranges `{1,7}`) to prevent catastrophic backtracking when processing untrusted inputs.
### 2. SSE Snapshot Handling
When parsing streaming LLM responses (e.g. Responses API), check if a chunk represents a final snapshot (`done` or `completed` events). Snapshot text must be sanitized directly as a standalone string (bypassing rolling delta buffers) to prevent text duplication at the end of the stream.
### 3. Database Handles in Tests
Ensure that any unit tests that trigger database migrations or establish SQLite connections call `resetDbInstance()` and properly clean up/close all DB handles in a `test.after(...)` hook. Failure to release database connection handles will cause Node's native test runner to hang indefinitely.
Before cutting a branch or opening a PR, run the base-green check (`AGENTS.md` → Git Workflow →
"Base-green check"; project skills reference it as `.agents/skills/_shared/base-green.md`). A PR
opened while the base tip is red must carry `⚠️ base-red inherited: #<issue>` in its body. To
drain an accumulated red state (base tip + red PRs), use the `/sweep-reds` skill.
> and the local development access notes that used to live in this file.
## 1. File Placement & Organization
Gemini-specific notes:
-**Test Files**: ALL unit tests, integration tests, ecosystem tests, or Vitest files MUST strictly be placed within the `tests/` directory (e.g., `tests/unit/`, `tests/integration/`). NEVER create test files in the project root (`/`).
- **Scripts and Utilities**: ALL maintenance, debugging, generation, or experimental scripts (`.cjs`, `.mjs`, `.js`, `.ts`) MUST be placed strictly inside one of the `scripts/` subfolders (`build/`, `dev/`, `check/`, `docs/`, `i18n/`, `ad-hoc/`). One-shot or experimental code goes under `scripts/ad-hoc/`. NEVER dump loose scripts in the project root (`/`) or the top-level `scripts/` folder.
When creating _any_ validation tests or one-off logic scripts, default to using `scripts/ad-hoc/` or the `tests/unit/` directories according to your goals. Do not pollute the `/` root context.
## 2. Hard Rules (mirror of `CLAUDE.md`)
1.**Never commit secrets or credentials.** Use `.env` (auto-generated from `.env.example`) or a vault. Passwords, OAuth secrets, API keys, and Cookie values must never appear in committed files.
2.**Never add logic to `src/lib/localDb.ts`.** It is a re-export barrel only.
3.**Never use `eval()`, `new Function()`, or any implied eval.** ESLint enforces this.
4.**Never commit directly to `main`.** Use `feat/`, `fix/`, `refactor/`, `docs/`, `test/`, or `chore/` branches.
5.**Never write raw SQL in routes** — always go through `src/lib/db/` domain modules.
6.**Never silently swallow errors in SSE streams** — propagate them or abort the stream cleanly.
7.**Never bypass Husky hooks** (`--no-verify`, `--no-gpg-sign`) without explicit operator approval.
8.**Always validate inputs with Zod schemas** from `src/shared/validation/schemas.ts`.
9.**Always include tests when changing production code** (`src/`, `open-sse/`, `electron/`, `bin/`).
10.**Coverage must stay** ≥ 60 % statements / lines / functions / branches — the official CI gate (`npm run test:coverage`). The ratchet baseline in `quality-baseline.json` may freeze a higher floor; never regress it.
The dashboard is reachable at the operator's chosen URL/port (default `http://localhost:20128`). Credentials are operator-specific:
- **Initial admin password** is read from the `INITIAL_PASSWORD` env var on first install (defaults to `CHANGEME` in `.env.example`; rotate immediately after first login).
- **Local VPS / shared dev environments**: ask the operator for the URL and current credentials — they live in their personal vault, NOT in this repo.
> Any credential observed in a previous version of this file was a non-production demo value; treat it as compromised and do not reuse it.
-Skills activate via the `activate_skill` tool (skill metadata is loaded at session start and
the full content is activated on demand).
- There are no other Gemini-only rules today. Do not re-add project rules here — edit
`AGENTS.md` instead, so every assistant sees the same instructions.
<img src="./docs/diagrams/promise-pillars.svg" width="100%" alt="The Promise — One endpoint. 290 providers. Never stop building — OmniRoute picks the cheapest one that works. Six pillars: Never hit limits (auto-fallback across 290 providers in milliseconds, zero downtime) · Save up to 95% tokens (RTK + Caveman stacked compression cuts 15–95%, ~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 15–95%, ~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)."/>
<b>What Kimi's support powers:</b> Kimi's API credits power OmniRoute's AI-validated release pipeline — the <i>merge validation powered by Kimi K3</i> stage that reviews every pull request before it ships — plus day-to-day feature development. First-class Kimi support ships on both rails: the direct <a href="https://platform.kimi.ai?aff=omniroute">Kimi API</a> (<code>kimi-k3</code>) and the <a href="https://www.kimi.com/code?aff=omniroute">Kimi Code coding plan</a> (OAuth and API key). OmniRoute is also the first Brazilian open-source project in Kimi's support program. <a href="https://platform.kimi.ai?aff=omniroute"><b>Get a Kimi API key →</b></a>
Thanks to <b>Cheaper Inference</b>, an OmniRoute Open Source Friend, for backing this project! Cheaper Inference is a cost-ranked gateway that resells 42 frontier models — Claude, GPT-5.x, Gemini, Kimi K3, GLM, DeepSeek, Grok and MiniMax — behind one OpenAI-compatible endpoint, routing each request to the cheapest eligible provider without ever charging above the model maker's list price.
<br/><br/>
<b>First-class support in OmniRoute:</b> Chat Completions, the native <code>/v1/responses</code> endpoint, vision, tool calling and 3 image models (<code>grok-imagine</code>, <code>nano-banana-pro</code>, <code>nano-banana-2</code>, reachable as <code>cheaperinference/<model></code>). <a href="https://cheaperinference.com/?utm_source=omniroute"><b>Get an API key →</b></a>
</td>
</tr>
</table>
<sub>Links tagged <code>aff=omniroute</code> are partner links. They fund the project at no extra cost to you.</sub>
<br/>
<details open>
<summary><sub><b>🎟️ Affiliates Promo</b> — free signup coupons from providers we don't sponsor (click to expand)</sub></summary>
<sub><i>This section is for referral/coupon codes only. Sponsored partnerships live in <b>🤝 Supported by our Open Source Friends</b> above. OmniRoute has no sponsorship or partnership with the providers listed here — these are public coupons anyone can use.</i></sub>
<sub><b><a href="https://agentrouter.org/register?aff=70LM">AgentRouter</a></b> — affiliate signup · <b>$100 free credits</b> on signup (free server, expect higher latency — best for testing, not production). First-class support in OmniRoute since <b>v3.8.50</b>: Chat Completions, the Anthropic-compatible wire format and the OpenAI-compatible path. Available models include <code>claude-opus-4-8</code>, <code>claude-opus-5</code>, <code>gpt-5.6-sol</code> and more. <b><a href="https://agentrouter.org/register?aff=70LM">Grab your $100 →</a></b></sub>
<br/><br/>
<sub>⚠️ <i>Affiliate link — OmniRoute has no sponsorship or partnership with this provider.</i></sub>
</td>
</tr>
</table>
<sub>Know another provider with a generous free signup coupon that benefits OmniRoute users? Open an issue and we'll add it here.</sub>
</details>
<br/>
<div align="center">
## 🎯 Combos — The Flagship
@@ -398,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: 290 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'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's docs."/>
<sub>📊 Full methodology & per-feature detail vs 9router, OpenRouter, CLIProxyAPI & LiteLLM → [`docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md`](docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md)</sub>
@@ -411,7 +452,6 @@ OmniRoute is MIT-licensed and maintained in the open. If it saves you time or mo
<tr><td nowrap>☕ <b>Ko-fi</b></td><td>Quick one-off tip, no signup for the donor</td><td><a href="https://ko-fi.com/diegosouzapw">ko-fi.com/diegosouzapw</a></td></tr>
<tr><td nowrap>🧋 <b>Buy Me a Coffee</b></td><td>Small, informal gesture</td><td><a href="https://www.buymeacoffee.com/diegosouzapw">buymeacoffee.com/diegosouzapw</a></td></tr>
<tr><td nowrap>🖐 <b>Liberapay</b></td><td>Recurring · non-profit · open source</td><td><a href="https://liberapay.com/diegosouzapw">liberapay.com/diegosouzapw</a></td></tr>
@@ -473,7 +513,7 @@ Pix copia-e-cola:
- **🖼️ New endpoints** — `/v1/ocr` (Mistral OCR) and `/v1/audio/translations` (Whisper-style) round out the media surface. → [API Reference](docs/reference/API_REFERENCE.md)
- **🎨 Image / video / audio generation** — one API for media: xAI Grok Imagine & Novita AI video, ComfyUI, Freepik, Adobe Firefly, Microsoft Designer, Google Imagen, Segmind, EdgeTTS. → [API Reference](docs/reference/API_REFERENCE.md)
- **🤝 More providers & agents** — Cursor Cloud Agent, Grok Build (xAI) with browser + OAuth login, Ollama first-class card, Claude Opus 5 & Sonnet 5, Kimi official partnership (Code/Web/Moonshot), Zed, Requesty, SenseNova, Yuanbao, Agnes AI… and a refreshed **290-provider catalog**. → [Providers](docs/reference/PROVIDER_REFERENCE.md)
- **🤝 More providers & agents** — Cursor Cloud Agent, Grok Build (xAI) with browser + OAuth login, Ollama first-class card, Claude Opus 5 & Sonnet 5, Kimi official partnership (Code/Web/Moonshot), Zed, Requesty, SenseNova, Yuanbao, Agnes AI… and a refreshed **291-provider catalog**. → [Providers](docs/reference/PROVIDER_REFERENCE.md)
- **📡 Routing transparency** — every response carries an `X-OmniRoute-Decision` header naming the strategy/provider/latency that served it, a new `cache-optimized` combo strategy + Auto-Combo `cacheAffinity` factor route repeat requests back to the connection holding the cached prefix, and a read-only `/v1/auto-combo/{channel}/candidates` endpoint exposes an `auto/*` channel's live candidate pool. → [Auto-Combo](docs/routing/AUTO-COMBO.md)
- **⚡ Local performance & infra** — one-click local Redis, Cloudflare Workers / Deno Deploy relay deployers, Bifrost & Mux as supervised embedded services. → [Embedded Services](docs/frameworks/EMBEDDED-SERVICES.md)
# Relatorio de pesquisa: repositorios de CLI integraveis com OmniRoute
> **Status final (2026-08-03):** este documento preserva o inventário inicial. A pesquisa foi concluída para `104/104` casos. Para resultados por projeto, use `04-tracker-integracoes-clis.md`; para o fechamento executivo e a estratégia de publicação, use `06-relatorio-final-104-clis-e-estrategia-prs.md`.
**Data da pesquisa:** 2026-08-01
**Escopo:** agentes de codigo de terminal, CLIs de LLM, runtimes de agentes e harnesses que possam consumir um endpoint HTTP compativel com OpenAI, Anthropic ou Gemini, ou que possam ser adaptados por provider/plugin/ACP/MITM.
**Fonte local principal:**`_tasks/hands-off/2026-08-01_release-v3.8.50_v3.8.50_sess-e1846bc2/handoff.md`
**Fontes externas principais:** GitHub Search/API, READMEs dos repositorios e a lista publica `bradAGI/awesome-cli-coding-agents` (atualizada em 2026-07-29).
## 1. Resumo executivo
O OmniRoute ja possui uma integracao funcional com o jcode e um catalogo local de ferramentas CLI. O proximo ganho de maior valor e transformar o OmniRoute em um endpoint reconhecido pelos principais agentes de terminal, priorizando configuracao nativa e PR upstream quando o projeto aceitar contribuicoes.
A pesquisa encontrou:
- **33 entradas de ferramentas no registro local `CLI_TOOLS`**, contando o registro extraido de Grok Build em `src/shared/constants/cliToolsGrokBuild.ts`, incluindo Claude Code, Codex CLI, Cline, Kilo, Continue, OpenCode, Aider, jcode, Smelt, Pi, Crush, Goose, Open Interpreter, OpenClaw, Hermes Agent, Letta CLI e outros.
- **Mais de 90 projetos publicos** no inventario externo consultado, entre agentes de codigo, CLIs generalistas, forks, runtimes e orquestradores.
- **Candidatos com evidencia forte de endpoint customizavel:** Gemini CLI, Claw Code, Plandex, MiMo Code, Trae Agent, Kimi CLI, Every Code, Open Codex, VT Code, OpenHands CLI, gptme, Nanocoder, RA.Aid, CoreCoder, Grok CLI, Gitlawb Zero, DeepSeek Reasonix, KlaatCode, CodeMini, DvalinCode, Coro Code, Mini-Kode, Late CLI, Agentty, Aizen, Minacode, YottaCode, aichat, ShellGPT, Mistral Vibe, OpenSquilla, Kode CLI e outros.
- **Candidatos que exigem pesquisa confirmatoria:** projetos com README generico, configuracao recente, repositorio ambiguo, binario fechado ou sem evidencia textual suficiente de `base_url`/provider.
- **Candidatos que podem ser integrados por outros caminhos:** ACP, MCP, wrapper/launcher, provider adapter, proxy MITM ou apenas documentacao; eles nao devem ser classificados automaticamente como OpenAI-compatible.
Conclusao: devemos pesquisar e tentar todos os candidatos tecnicamente viaveis, mas separar claramente `suporte no catalogo OmniRoute`, `configuracao generica`, `adaptacao upstream publicada` e `PR/issue aceita`. O tracker acompanha essas dimensoes separadamente.
## 2. Metodo e limites
### 2.1 Como a busca foi feita
1. Leitura integral do handoff do caso jcode para capturar o padrao de integracao, validacao, publicacao e as restricoes de worktree.
2. Inspecao do catalogo local em `src/shared/constants/cliTools.ts`, da documentacao de CLI e do fluxo de setup em `docs/guides/CLI-INTEGRATIONS.md`.
3. Consulta do GitHub Search/API para resolver o repositorio canonico de cada nome, evitando homonimos.
4. Leitura de README/raw quando disponivel, procurando sinais como `base_url`, `baseURL`, `OPENAI_BASE_URL`, `OPENAI_API_BASE`, `LLM_BASE_URL`, `provider`, `gateway`, `model provider`, `Anthropic` e `Gemini`.
5. Consulta da lista `https://github.com/bradAGI/awesome-cli-coding-agents`, que serve como descoberta ampla, nao como prova de compatibilidade.
6. Classificacao por adocao, manutencao, licenca, evidencia de endpoint, maturidade, potencial de PR e utilidade para o ecossistema OmniRoute.
### 2.2 O que ainda nao foi afirmado
- Nao foi feita implementacao ou abertura de PR/issue para os candidatos abaixo; o unico caso publicado nesta sessao anterior e o jcode.
- A presenca da palavra `provider` no README nao prova que uma URL arbitraria funciona em runtime.
- Estrelas e datas sao snapshots aproximados obtidos em 2026-08-01 e podem mudar.
- Repositorios fechados ou com EULA entram no inventario para avaliacao de configuracao, mas nao implicam possibilidade de fork ou PR.
- Cada task de integracao precisa repetir a pesquisa no upstream antes de editar codigo.
## 3. Baseline do OmniRoute
### 3.1 Superficie que o OmniRoute oferece
- Endpoint OpenAI em `/v1`.
- Superficie Anthropic na raiz, usada por clientes que esperam `/v1/messages` a partir do `ANTHROPIC_BASE_URL`.
- Superficie Gemini em `/v1beta`.
- Catalogo de modelos consultavel pelos comandos de setup quando o cliente suporta descoberta.
- Chave via `OMNIROUTE_API_KEY` ou chave selecionada no dashboard.
- Traducao entre formatos, streaming SSE, tool calling, fallback, combos, custos e politicas de autenticacao.
- Modos de consumo: configuracao de ambiente, arquivo nativo do cliente, provider customizado, ACP/MCP e MITM.
### 3.2 Catalogo local ja registrado
Fonte: `src/shared/constants/cliTools.ts` e `src/shared/constants/cliToolsGrokBuild.ts`.
**Agentes:** Hermes, Hermes Agent, Goose, Open Interpreter, Oh My Pi, Letta CLI, Warp AI, Agent Deck.
Os documentos do catalogo tambem mantem um backlog MITM para ferramentas sem base URL, como Windsurf, Amp, Amazon Q/Kiro CLI e Cowork. Esses casos devem permanecer separados de uma integracao direta.
### 3.3 Caso jcode (referencia validada)
- Upstream: `https://github.com/1jehuang/jcode`
- Mecanismo: perfil OpenAI-compatible dirigido por metadados; nao foi criado um plugin de runtime.
- Branch: `feat/omniroute-provider`
- Commit: `ee4f904e6`
- PR no fork: `https://github.com/diegosouzapw/jcode/pull/1`
- Issue no upstream: `https://github.com/1jehuang/jcode/issues/704`
- Diff: 6 arquivos, `+56/-3`.
- Validacao: `cargo check --workspace` limpo; 205 testes passaram e uma falha foi preexistente/ambiental.
- Estado: aguardando mantenedor; o upstream nao aceita PR de forks externos, por isso a issue e o artefato oficial.
- Pendencia prometida: adicionar no README do OmniRoute a secao "Tools & repositories that work with OmniRoute".
Licao: o trabalho deve comecar descobrindo o mecanismo real de providers do upstream. Nem todos os clientes precisam de mudanca no OmniRoute; alguns precisam somente de um perfil local, e outros exigirao um adaptador especifico.
## 4. Candidatos prioritarios com evidencia concreta
As evidencias abaixo sao sinais de README/configuracao observados na pesquisa inicial. A task individual deve abrir o arquivo exato, confirmar a versao atual e executar um smoke test.
| Kode CLI | `shareAI-lab/Kode-cli` | provider, endpoint e Anthropic/OpenAI/Gemini | config |
| Crush | `charmbracelet/crush` | `base_url`, provider compativel | ja catalogado no OmniRoute; validar upstream |
| Hermes Agent | `NousResearch/hermes-agent` | endpoint/gateway e 300+ modelos | ja catalogado; validar modo de endpoint |
| OpenClaw | `openclaw/openclaw` | providers, gateway e endpoints | ja catalogado; validar configuracao atual |
## 5. Inventario amplo localizado
### 5.1 Agentes de terminal e coding CLIs
Os projetos desta tabela foram encontrados na lista curada ou no GitHub Search. `Pesquisa` indica o proximo gate; nao significa que a integracao ja esta pronta.
### 5.2 Agentes generalistas e ecossistema OpenClaw
Estes podem consumir OmniRoute como backend, mas a task deve confirmar se a interface de configuracao e realmente uma CLI de codigo ou apenas um gateway de agente.
| Projeto | Repositorio | Possivel caminho |
|---|---|---|
| OpenClaw | `openclaw/openclaw` | provider/gateway; ja catalogado |
- **`/v1` duplicado:** alguns clientes recebem a raiz e acrescentam `/v1/chat/completions`; outros exigem a URL final com `/v1`. Cada task deve registrar o resultado real.
- **Chat Completions vs Responses:** forks do Codex e clientes modernos podem usar Responses; testar ambas quando o cliente permitir.
- **Anthropic:** clientes que mandam `/v1/messages` esperam `ANTHROPIC_BASE_URL` sem `/v1` no valor. A traducao Anthropic do OmniRoute deve ser validada com streaming e tool use.
- **Gemini:** Gemini CLI pode esperar uma base Gemini nativa, nao somente OpenAI-compatible; validar `generateContent`, streaming e headers.
- **Tool calling:** o agente pode exigir nomes/ids de ferramenta estaveis, JSON estrito, `tool_choice` ou blocos de pensamento especificos.
- **Descoberta de modelos:** `/v1/models` pode ser obrigatorio, opcional ou inexistente. O setup precisa aceitar `--model` fixo quando a descoberta nao for suportada.
- **Autenticacao:** alguns projetos leem somente env, outros gravam tokens em arquivo/keyring e alguns usam OAuth proprietario. Nunca reutilizar credenciais de um upstream sem verificar escopo.
- **Streaming e retry:** SSE, timeouts, abort signals e re-tentativas podem divergir do cliente. Validar uma chamada longa e uma falha de provider.
- **Licenca:** GPL/AGPL, EULA e repositorios sem SPDX exigem decisao de distribuicao antes de enviar patch.
## 7. Riscos de pesquisa e integracao
1.**Homonomimos e clones:** usar sempre URL canonica, organizacao, release e README do repositorio correto.
2.**Repositorios que mudam rapidamente:** congelar commit/versao no relatorio da task e repetir a consulta no dia da implementacao.
3.**README divergente do codigo:** procurar schema, parser de config, testes e comando de execucao; README sozinho e evidencia Tier 1.
4.**Clientes fechados:** registrar como `needs-mitm` ou `config-only`, nunca como PR upstream.
5.**Forks com historia de origem controversa:** avaliar politica, licenca e aceite de contribuicoes antes de reproduzir componentes.
6.**Segredos no ambiente:** limpar `OMNIROUTE_API_KEY` e chaves de teste quando a suite assume ambiente sem credencial, como ocorreu no jcode.
7.**Mudancas no checkout:** usar worktree em `.claude/worktrees/` por projeto; nao editar o checkout compartilhado do OmniRoute nem usar `git stash`.
## 8. Recomendacao
Executar primeiro os lotes P0/P1 do documento de prioridade. Cada lote pode ter ate tres subagentes, um repositorio por worktree. O agente principal deve revisar a pesquisa, o smoke test e a licenca antes de permitir implementacao. O resultado de cada caso deve atualizar o tracker com commit, PR/issue, validacao e status upstream, sem preencher campos externos por suposicao.
> **Status final (2026-08-03):** esta é a priorização inicial que orientou a execução. Todos os `104/104` casos já foram pesquisados. A classificação final está no tracker `04`; a estratégia revisada de contribuição está no relatório `06`.
**Snapshot:** 2026-08-01
**Objetivo:** ordenar do melhor para o pior todos os projetos tecnicamente candidatos a consumir OmniRoute, sem remover projetos pequenos. A ordem e uma fila de pesquisa/execucao; ela nao e promessa de que todo upstream aceitara um PR.
## Como ler a prioridade
- **P0:** ja esta no catalogo OmniRoute ou tem evidencia muito forte de endpoint customizavel; executar/consolidar primeiro.
- **P1:** forte candidato novo, com provider/base URL evidente e bom retorno para o ecossistema.
- **P2:** tecnicamente promissor, mas requer confirmacao de protocolo, config, maturidade ou licenca.
- **P3:** possivel via ACP/MCP/wrapper/launcher, ou com menor adocao; pesquisar depois dos P0-P2.
- **P4:** cliente fechado, EULA, MITM ou pesquisa exploratoria; manter no inventario, mas nao bloquear os demais.
Os fatores usados foram: evidencia de endpoint arbitrario, adocao/atividade, facilidade de teste, compatibilidade OpenAI/Anthropic/Gemini, maturidade, licenca, chance de PR upstream, valor para usuarios OmniRoute e risco de protocolo.
## A. Catalogo OmniRoute ja existente
Estas entradas ja aparecem no registro local. A prioridade aqui significa consolidar documentacao, smoke tests, detector/configurador e eventual upstream nominal; nao significa recriar uma integracao que ja existe.
| Ordem | Projeto | Repositorio/documentacao | Estado local | Proximo foco |
|---:|---|---|---|---|
| A1 | Claude Code | `anthropics/claude-code` | catalogado; Anthropic base URL | manter compatibilidade Anthropic, streaming e tools |
Um projeto sobe de prioridade quando a pesquisa individual confirma: configuracao documentada, teste local com OmniRoute, licenca permissiva e contribuicao aceita. Desce quando: a URL e fixa, o endpoint e somente SaaS, o README nao corresponde ao codigo, a autenticacao e inseparavel do provedor, ou a licenca/EULA impede redistribuicao. Nenhum projeto e marcado como impossivel sem registrar a evidencia no tracker.
> **Status final (2026-08-03):** a fase de pesquisa foi concluída em lotes de até três worktrees/agentes, cobrindo `104/104` casos. Este documento continua válido como processo operacional para implementação/publicação. Consulte `06-relatorio-final-104-clis-e-estrategia-prs.md` para o resultado final.
**Data:** 2026-08-01
**Objetivo:** pesquisar, integrar, validar e publicar suporte ao OmniRoute em todos os projetos tecnicamente possiveis, mantendo uma fila que permite ate tres subagentes simultaneos.
O ciclo especifico de preparacao, revisao, envio e acompanhamento das contribuicoes upstream esta
em `05-plano-publicacao-prs-upstream.md`.
## 1. Principios operacionais
- Um repositorio por subagente e por worktree.
- No maximo tres tasks de repositorios em execucao ao mesmo tempo.
- Cada task pesquisa o upstream novamente antes de editar; o relatorio inicial e somente contexto.
- O agente principal revisa licenca, arquitetura, smoke test e diff antes do proximo lote.
- Nao usar checkout compartilhado para desenvolvimento e nao usar `git stash`/`git pop`.
- Usar worktrees em `.claude/worktrees/` e branches especificas.
- Nao inventar PR, issue, commit ou aceite de mantenedor.
- Nao adicionar trailers ou rodapes de IA em commits/PRs.
## 2. Fases obrigatorias por projeto
### Fase 0 - Preparacao da task
Criar uma task com nome do projeto, URL canonica, prioridade, evidencia inicial, estado no catalogo OmniRoute e objetivo de integrar. Definir a worktree e o agente responsavel.
### Fase 1 - Pesquisa individual fresca
O agente deve verificar no upstream atual:
- arquitetura de providers e ponto de entrada do CLI;
- arquivo/schema de configuracao e suporte a `base_url`, `baseURL`, `OPENAI_BASE_URL`, `OPENAI_API_BASE`, `LLM_BASE_URL` ou equivalente;
- protocolo real (Chat Completions, Responses, Anthropic Messages, Gemini, ACP, MCP ou outro);
- descoberta de modelos e necessidade de `/v1/models`;
- autenticacao, keyring, OAuth e variaveis de ambiente;
- streaming, tool calling, reasoning e limites conhecidos;
- politica de contribuicao, licenca e se PR de fork externo e aceito;
- atividade, releases, issues/PRs sobre providers customizados ou endpoints locais;
- comandos de build, lint, teste e smoke test;
- possibilidade de fork/PR, issue de proposta, documentacao ou apenas wrapper/MITM.
Registrar commit/release pesquisado e links de evidencia.
### Fase 2 - Gate de viabilidade
Classificar exatamente um caminho inicial:
`viable-direct` (somente configuracao), `viable-upstream` (mudanca no upstream), `viable-acp`, `viable-mcp`, `needs-wrapper`, `needs-mitm`, `config-only`, `blocked` ou `research-more`.
Nao implementar antes de haver uma conclusao de viabilidade e uma razao verificavel.
### Fase 3 - Baseline e TDD
- Executar a suite recomendada pelo upstream antes das mudancas.
- Registrar falhas preexistentes, dependencias ausentes e comandos exatos.
- Limpar `OMNIROUTE_API_KEY` e demais credenciais quando os testes pressupuserem ambiente sem chaves.
- Adicionar primeiro um teste de configuracao, endpoint e selecao de modelo que falhe sem a integracao.
### Fase 4 - Implementacao minima
Implementar apenas o necessario para o caso pesquisado:
- perfil/preset `omniroute` ou provider custom;
- base URL correta (raiz, `/v1` ou `/v1beta` conforme o cliente);
- chave via ambiente ou mecanismo seguro do cliente;
- modelo fixo ou descoberta de modelos;
- selecao/login/report se o CLI tiver esses fluxos;
- documentacao de uso e limites;
- testes de config e chamada.
Se o upstream nao aceitar mudanca, preparar wrapper/launcher ou documentacao local e registrar a limitacao.
### Fase 5 - Validacao funcional
Executar, conforme o protocolo:
- build, lint, typecheck e testes do upstream;
- smoke request com OmniRoute;
- streaming SSE e encerramento por abort;
- tool calling e JSON de argumentos;
-`/v1/models` ou equivalente;
- Chat Completions, Responses, Anthropic Messages e Gemini `generateContent` quando aplicavel;
- fallback/erro, timeout, retry e modelo inexistente;
- teste com chave limpa e teste com `OMNIROUTE_API_KEY` real fora dos logs.
### Fase 6 - Publicacao upstream
- Criar fork somente quando permitido e branch especifica.
- Abrir PR upstream se contribuicoes externas forem aceitas.
- Se PR externo for bloqueado, abrir issue com proposta, patch/referencia e smoke test.
- Se o projeto for fechado/EULA, registrar config manual ou issue de produto; nao criar PR ficticio.
- Atualizar o tracker com URL, commit, estado e resposta do mantenedor.
### Fase 7 - Catalogo e integracao OmniRoute
Quando houver valor para usuarios OmniRoute:
- criar worktree propria do OmniRoute;
- atualizar `src/shared/constants/cliTools.ts` ou `src/shared/constants/cliToolsGrokBuild.ts`;
- atualizar detector em `src/lib/cli-helper/tool-detector.ts` se necessario;
- adicionar gerador/configurador e rota de settings somente se o caso exigir;
- adicionar testes do catalogo, detector, settings, `baseUrlSupport` e `/v1`;
- atualizar `docs/reference/CLI-TOOLS.md`, `docs/guides/CLI-INTEGRATIONS.md` e README quando apropriado;
- atualizar o tracker com a integracao local e evidencias.
### Fase 8 - Fechamento
Registrar commit, branch, PR/issue, testes, limitacoes, status do upstream, status do catalogo OmniRoute e proximo passo. O agente principal faz uma revisao final de seguranca, licenca e factualidade.
## 3. Lotes de ate tres subagentes
O lote e uma unidade operacional. A fila abaixo e ordenada pelo documento `02-prioridade-integracoes-clis.md`; cada linha representa uma task individual.
### Lote 0 - consolidacao do caso de referencia
-`CLI-000` - jcode - manter a issue #704, validar resposta do mantenedor e concluir a secao do README OmniRoute.
O lote seguinte pode iniciar quando os tres agentes do lote atual tiverem: pesquisa upstream anexada, gate de viabilidade preenchido, baseline registrado, resultado de smoke test ou bloqueio reproduzivel, e tracker atualizado. Uma falha de um agente nao deve paralisar os outros dois; o agente principal deve marcar `blocked` ou `research-more` com evidencia e seguir a fila.
## 5. Entregaveis de cada task
1. Nota de pesquisa fresca com commit/release e links.
2. Classificacao de viabilidade.
3. Diff minimo ou conclusao documentada de que nao ha diff necessario.
4. Testes e comandos executados, incluindo falhas preexistentes.
5. PR/issue upstream ou justificativa de config-only/MITM.
6. Entrada no catalogo OmniRoute quando aplicavel.
7. Atualizacao do tracker `04-tracker-integracoes-clis.md`.
**Status final da pesquisa:**`104/104` concluídos (`100%`), `0` casos `not-started`. Este é o registro individual autoritativo. O relatório executivo está em `06-relatorio-final-104-clis-e-estrategia-prs.md`.
Os campos externos (`branch`, `commit`, `PR`, `issue`) ficam como `—` ate haver evidencia real. “Catalogo OmniRoute” significa entrada local, nao necessariamente suporte upstream publicado.
| ID | Prio | Projeto | Repositorio | Pesquisa | Tipo | Upstream | Branch | Commit | PR | Issue | Catalogo OmniRoute | Observacoes/proximo passo |
| CLI-037 | P1 | Deep Agents Code | `langchain-ai/deepagents` | concluida | `config-only` | not-applicable | `feat/omniroute-deepagents-code-integration` | — | — | — | not-in-catalog | SHA `46ee772b4`; `deepagents-code==0.1.51`; provider `openai`, base OmniRoute `/v1`, model `openai:auto`; Responses e default, Chat usa `use_responses_api=false`; smoke de config verde, sem HTTP/runtime por deps e disco; #3973/#3287 ja cobrem os pontos genericos; sem publicacao nominal |
| CLI-038 | P1 | OpenHands principal | `OpenHands/OpenHands` | concluida | `config-only` | not-applicable | `feat/omniroute-openhands-main-integration` | — | — | — | not-in-catalog | SHA `1708efc44`; Agent Canvas `1.8.0`; `openai/auto` + base `/v1` + API key + `api_mode=chat`; LiteLLM envia `model=auto`, Chat/SSE/tools estruturais; sem discovery generico `/v1/models`; PRs OmniRoute [#15189](https://github.com/OpenHands/OpenHands/pull/15189)/[#15211](https://github.com/OpenHands/OpenHands/pull/15211) fechadas sem merge; sem nova publicacao |
| CLI-039 | P1 | SWE-agent | `SWE-agent/SWE-agent` | concluida | `config-only` | not-applicable | `feat/omniroute-swe-agent-integration` | — | — | — | not-in-catalog | SHA `3ea751c08`; release `v1.1.0`; LiteLLM com `openai/<model-id>`, `api_base=/v1` e chave por env; Chat/tools/tool round-trip e batch confirmados por fonte; reasoning parcial; smoke HTTP bloqueado por deps ausentes; sem publicacao nominal |
| CLI-040 | P1 | AutoCodeRover | `AutoCodeRoverSG/auto-code-rover` | concluida | `pr-generic` | `validating` | `feat/omniroute-auto-code-rover-integration` | — | — | — | not-in-catalog | SHA `585d3e639`; patch local sem commit em 4 arquivos corrige `litellm-generic-openai/auto`, base `/v1`, precedencia da chave e pricing desconhecido; 9 testes focados com stubs, tracer source-only, compileall e diff-check verdes; sem HTTP real; licenca SONAR Source-Available exige gate juridico antes de publicar |
| CLI-041 | P2 | Claurst | `Kuberwastaken/claurst` | concluida | `config-only` | not-applicable | `feat/omniroute-claurst-integration` | — | — | — | not-in-catalog | SHA `595b0ebe3`; `custom-openai` com settings persistidos, base `/v1`, `CUSTOM_OPENAI_API_KEY`, modelo `auto`, Chat/SSE/tools e `/v1/models`; CI upstream verde; sem build/smoke local e sem publicacao nominal; monitorar PR #365 sem duplicar |
Ao terminar uma fase, alterar somente os campos comprovados e deixar os demais como `—`. Para uma integracao concluida, registrar: versao/commit pesquisado, mecanismo, arquivos modificados, testes, branch, commit, URL de PR/issue e resposta do mantenedor. Se o caso for apenas configuracao, registrar o comando/config real e marcar `config-only` ou `viable-direct`, sem criar uma PR artificial.
Antes de publicar uma contribuicao, aplicar o gate e o checklist de
# Plano de publicacao de integracoes OmniRoute nos repositorios upstream
> **Status da campanha de pesquisa:** `104/104` casos concluídos. Este plano continua sendo o procedimento de execução e publicação. A matriz final, inclusive os casos em que PR é inadequada ou impossível, está em `06-relatorio-final-104-clis-e-estrategia-prs.md`.
**Data:** 2026-08-01
**Escopo:** transformar a fila `CLI-000` a `CLI-103` em contribuicoes upstream verificadas,
publicando PR, issue, guia de configuracao, adaptador ou conclusao de bloqueio conforme o mecanismo
Os tres subagentes fazem pesquisa fresca e implementacao em paralelo, mas nenhuma PR e enviada antes
da revisao individual do agente principal. Ao publicar ou concluir config-only/bloqueio, atualizar o
tracker e liberar os mesmos tres slots para o lote P0.2.
## Lote P2.10 iniciado em 2026-08-03
Base local: `release/v3.8.50` em `84b1e5e12f238269e698f400766230f985f4a07b`. Worktrees isoladas e um agente por upstream foram criadas para `CLI-070` PicoClaw, `CLI-071` IronClaw e `CLI-072` NullClaw. Nenhuma publicação está autorizada; os agentes devem pesquisar o HEAD atual, provar `config-only` ou RED→GREEN e registrar governança, gates, smoke e estado limpo.
Resultado P2.10:
-`CLI-070` PicoClaw: `config-only`, `openai/auto` com base `/api/v1`; Chat/SSE/tools/usage/images/discovery. Go ausente impediu execução local; monitorar #3298, sem PR.
-`CLI-071` IronClaw: `config-only`, `openai_compatible` com `/api/v1` e `auto`; 889 testes do crate LLM, 5 de resolução e fmt passaram. Sem PR; reasoning proprietário segue limitado por #3673.
-`CLI-072` NullClaw: `config-only`, provider custom com Chat Completions recomendado e Responses/Anthropic como alternativas. Zig ausente; CI do mesmo HEAD verde. Sem PR.
Estado final P2.10: commits `0`, pushes `0`, forks `0`, PRs `0`, issues `0`, Discussions `0`.
-`CLI-076` VibePod: `config-only` pelo agente Claude Code com raiz Anthropic `/api`; wrapper injeta env no container. Codex sem chave automática permanece não comprovado.
-`CLI-077` zeroshot: `config-only` pelo gateway OpenAI `/api/v1`; 22 testes focados verdes; limitações de streaming JSON, reasoning e MCP registradas.
-`CLI-078` Fractal: `config-only` por Codex Responses em `CODEX_HOME` por node; servidores tmux quentes podem perder `OMNIROUTE_API_KEY`, recomendando fix genérico upstream.
Resultado P3.2: Bernstein `config-only` por openai_agents; Traycer `config-only` indireto pelo harness OpenCode; h5i `patch-required` porque auth proxy/egress são fixados em OpenAI. Nenhuma publicação externa. Pesquisa acumulada `82/104` (`78,8%`), pendentes `22/104` (`21,2%`).
Resultado P2.11:
-`CLI-073` Moltis: `config-only`, provider `custom-omniroute`, `/api/v1`, `auto`, Chat/SSE/tools e capacidades multimodais. 401 testes e fmt passaram. Sem publicação.
-`CLI-074` GitClaw/GitAgent: `config-only`, loader OpenAI-compatible com `GITAGENT_MODEL_BASE_URL`, `OPENAI_API_KEY` e `omniroute:auto`. Build, 65 testes e smoke passaram. Sem publicação.
-`CLI-075` LionClaw: `patch-required`/`issue-first`. O runtime Codex confinado não recebe `config.toml`/provider secret; preparar proposta genérica alinhada à [#157](https://github.com/moshthepitt/lionclaw/issues/157), sem PR até revisão do mantenedor.
Resultado P3.4: LoopTroop `config-only` indireto via provider OpenCode; Galley `patch-required` por não possuir transport OpenAI-compatible configurável; Relay `config-only` via provider profile/Codex, condicionado a smoke da Responses API e controles sobre ferramentas nativas. Nenhuma publicação externa. Pesquisa acumulada `88/104` (`84,6%`), pendentes `16/104` (`15,4%`).
Resultado P3.5: SageCLI `config-only` indireto via Codex, com caveat de env plaintext; 5dive `patch-required` por mapas fechados de provider/base; agx `config-only` indireto via Codex e com gates de Responses/sandbox. Pesquisa acumulada `91/104` (`87,5%`), pendentes `13/104` (`12,5%`). Nenhuma publicação externa.
Resultado P3.6: claude-code-router, cc-router e OneCLI são config-only; os dois primeiros oferecem endpoints custom OpenAI-compatible e OneCLI injeta credenciais por proxy MITM. Pesquisa acumulada `94/104` (`90,4%`), pendentes `10/104` (`9,6%`). Nenhuma publicação externa.
Resultado P3.7: agent-browser `config-only` direto por Chat Completions; OpenWork `config-only` via OpenCode custom; Agent Deck `config-only` via CLIs filhos. Pesquisa acumulada `97/104` (`93,3%`), pendentes `7/104` (`6,7%`). Nenhuma publicação externa.
Resultado P4.1: Pool e Junie são `config-only` OpenAI-compatible; Cursor é `config-only` limitado ao BYO chat panel, sem MITM/protocolo privado. Pesquisa acumulada `100/104` (`96,2%`), pendentes `4/104` (`3,8%`). Nenhuma publicação externa.
Resultado P4.2: Windsurf está bloqueado para inferência e permite apenas MCP; Amp depende de confirmação Enterprise; Amazon Q legado requer patch substancial e Kiro atual é MCP-only seguro. Pesquisa acumulada `103/104` (`99,0%`), pendente `1/104` (`1,0%`). Nenhuma publicação externa.
Resultado P4.3: Cowork não permite substituir oficialmente a inferência; Custom Connector MCP remoto é o único caminho suportado e permanece separado do modelo. Pesquisa concluída `104/104` (`100%`), pendentes `0/104` (`0%`). Nenhuma publicação externa nesta fase de pesquisa.
# Relatório final — campanha de 104 integrações CLI OmniRoute
**Data de fechamento:** 2026-08-03
**Escopo:**`CLI-000` a `CLI-103`
**Resultado:**`104/104` pesquisados (`100%`), `0` pendentes de pesquisa.
## Como consultar o resultado individual
O documento autoritativo, com uma linha para cada caso, é o [tracker completo](./04-tracker-integracoes-clis.md). Ele contém para cada ID:
- prioridade;
- projeto e repositório;
- classificação de integração;
- estado de contribuição upstream;
- branch e commit quando existentes;
- URL de PR e/ou issue quando publicados;
- estado no catálogo OmniRoute;
- observações, limitações, testes e próximo passo.
Além do tracker, existem fichas técnicas individuais em `_tasks/cli-integrations/`. A cobertura foi auditada e agora há uma ficha para cada ID `CLI-000`–`CLI-103`; o caso `CLI-000` jcode foi adicionado como ficha de referência nesta revisão.
## Resumo quantitativo
| Grupo operacional | Quantidade | Tratamento |
|---|---:|---|
| Configuração direta ou indireta | 76 | Documentar receita, validar smoke e só abrir PR se houver melhoria upstream real |
| Contribuição upstream (PR/issue/docs/patch) | 17 | Preparar diff mínimo, validar, revisar e publicar conforme política do repositório |
| Patch obrigatório | 4 | Implementar genericamente, com RED→GREEN/TDD e revisão do mantenedor |
| Bloqueados/fechados | 4 | Registrar bloqueio; usar apenas MCP ou canal oficial, sem MITM |
| MCP/wrapper/ACP como caminho principal | 2 | Integrar a camada de ferramentas/orquestração, sem falsificar provider de inferência |
| Outros casos híbridos | 1 | Seguir a combinação específica descrita no tracker |
Os números são derivados do campo `Tipo` do tracker; categorias podem se sobrepor em casos híbridos. Atualmente há **7 PRs reais** e **9 issues reais** registrados no tracker, além de cinco entradas locais marcadas como integradas ao catálogo OmniRoute. Nenhum link foi inventado para os 97 casos sem publicação externa.
## O que foi feito na campanha
1. Inventário inicial e busca extensa de CLIs, runtimes, harnesses e control-planes.
2. Priorização P0–P4 considerando compatibilidade de protocolo, adoção, licença, maturidade e risco.
3. Pesquisa fresca, uma a uma, em worktrees isoladas, em lotes de no máximo três agentes.
4. Uso de Codebase Memory para índices upstream e verificação de cobertura; faixas parciais foram lidas diretamente quando aplicável.
5. Classificação por configuração, patch, PR documental, issue-first, MCP, wrapper ou bloqueio.
6. Registro de comandos, base URL, autenticação, modelos, streaming, tools, reasoning, imagens, MCP/ACP/A2A, testes e limitações.
7. Consolidação de cada lote com commit separado no OmniRoute e no repositório `_tasks`.
8. Atualização final do tracker, plano de integração, plano de publicação e handoff.
9. Nenhuma credencial real, publicação externa ou técnica de interceptação não autorizada foi utilizada.
## Estratégia para abrir PRs em 100% dos casos
“Abrir PR para 100%” deve ser interpretado como **dar um destino upstream apropriado a 100% dos casos**, e não criar 104 PRs artificiais. Há quatro trilhas:
### Trilha A — PR de código ou documentação
Aplicar aos casos `viable-upstream`, `pr-generic`, `pr-docs`, `patch-required` e híbridos que tenham superfície pública e política de contribuição compatível.
Processo por caso:
1. Reconfirmar HEAD, licença, branch default, política de contribuição e duplicatas.
2. Criar worktree/branch baseada na versão local vigente.
3. Executar baseline upstream e registrar falhas preexistentes.
4. Escrever teste RED que demonstre a lacuna.
5. Implementar o menor patch genérico possível — preferir `openai-compatible`, `base_url` ou provider abstrato a um provider nominal OmniRoute.
6. Executar GREEN: testes focados, suite upstream, lint, format, typecheck/build e smoke com fake server ou OmniRoute local usando placeholder.
7. Revisar segurança: nenhuma chave em argv, logs, fixtures, URL ou artefato; erros sanitizados; streaming/tools/cancelamento cobertos.
8. Abrir PR somente se contribuições externas forem aceitas. O corpo deve explicar problema, solução genérica, compatibilidade, testes, limitações e não conter marketing/texto de IA.
9. Se o repositório bloquear fork/PR ou pedir discussão prévia, abrir issue de proposta com o mesmo patch/reprodução, sem enviar PR prematuramente.
10. Atualizar tracker com branch, commit, URL, CI, revisão e resposta do mantenedor; acompanhar até `accepted`, `merged`, `rejected` ou `awaiting-maintainer`.
### Trilha B — Issue-first, discussão ou suporte ao mantenedor
Aplicar quando a arquitetura é adequada, mas há bloqueio de governança, firewall, CLA, fork fechado, dúvida de protocolo ou necessidade de decisão do autor. A issue deve conter:
- caso de uso OmniRoute;
- configuração atualmente possível;
- lacuna reproduzível;
- proposta genérica;
- impacto de segurança;
- testes/fake server;
- disposição para enviar PR após aprovação.
Não abrir uma PR paralela enquanto a política exigir issue-first.
### Trilha C — Config-only documentado
Aplicar aos casos em que o upstream já suporta a integração e uma mudança de código seria redundante. O entregável é:
- ficha individual;
- receita validada;
- smoke test e limitações;
- eventual documentação externa/local do OmniRoute;
- issue somente se houver pedido de documentação ou descoberta de bug real.
Não criar provider nominal ou PR apenas para adicionar a palavra “OmniRoute”.
### Trilha D — MCP, wrapper ou bloqueio seguro
Aplicar a control-planes, produtos fechados e CLIs sem rota de inferência substituível. O resultado pode ser:
- MCP remoto/stdio do OmniRoute;
- wrapper local claramente identificado como wrapper;
- solicitação oficial de custom provider;
- registro de bloqueio e gate legal/ToS.
Nunca mascarar OmniRoute como Claude/Codex, falsificar executável, interceptar TLS ou reutilizar tokens privados para fabricar uma PR upstream.
## Ordem recomendada de execução
1.**Primeiro:** PRs e issues já preparadas ou com alto retorno e baixo risco — jcode, Gemini CLI, Claw Code, Plandex, Trae Agent, Every Code, VT Code e CoreCoder.
2.**Segundo:** patches genéricos com boa superfície OSS — AutoCodeRover, Galley, 5dive e demais casos `pr-generic`/`patch-required`.
3.**Terceiro:** issues aguardando decisão — Open Codex, Kimi CLI, Devon, g3, Free Code, Claude Engineer e casos com `awaiting-maintainer`.
4.**Quarto:** documentação e receitas config-only agrupadas por ecossistema — OpenCode, Codex, LiteLLM, AI SDK, OpenAI-compatible e Anthropic-compatible.
5.**Quinto:** MCP/plugins para produtos fechados — Windsurf, Amp, Kiro, Cowork e Cursor, sempre pela superfície oficial.
Cada rodada deve manter no máximo três agentes ativos. O agente principal revisa o resultado do trio antes de liberar o próximo.
## Critério de encerramento por caso
Um caso só pode ser marcado como finalizado quando possui: pesquisa, classificação, evidência de protocolo, baseline ou limitação reproduzível, receita/patch/bloqueio, validação proporcional, estado de publicação e próximo passo. Para produtos fechados, `blocked-closed` ou `MCP-only` é um resultado válido e preferível a uma PR não autorizada.
## Estado de publicação atual
Os únicos links de publicação comprovados devem continuar sendo os registrados no tracker. O fato de existir uma branch local de pesquisa não significa que exista PR upstream. A matriz de verdade é:
- PR/issue preenchida: publicação real;
- campo `—`: nenhuma publicação externa comprovada;
-`not-applicable`: configuração ou bloqueio sem contribuição upstream;
-`published-pr`/`published-issue`: URL real presente no tracker.
## Próxima fase
A pesquisa está encerrada. A próxima fase é execução controlada da Trilha A/B/C/D, começando pelos casos com maior retorno e menor risco, com revisão central antes de qualquer push, PR, issue ou contato externo.
- **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)
- **feat(electron):** Desktop app can now attach to an already-running OmniRoute server (e.g. a Docker/OrbStack container) instead of always spawning its own bundled server — configurable via the tray's "Remote Server → Connect to Remote Server…" or the `OMNIROUTE_REMOTE_URL` env var ([#8799](https://github.com/diegosouzapw/OmniRoute/pull/8799)) — thanks @soulhakr
- **Providers**: expands the Novita AI catalog from a single Llama 3.1 8B entry to 19 curated serving models (DeepSeek V4, Kimi K3, GLM 5.2, MiniMax M3, Qwen3.7 Max, Qwen3 Coder 480B, MiMo V2.5 Pro, gpt-oss-120b, Gemma 4 31B and more), each carrying its real context window, output cap and reasoning flag from the live `/openai/v1/models` listing, and each vision flag confirmed by an actual image request rather than the listing's self-reported modalities
- **Database**: The `node:sqlite` fallback now uses SQLite's native backup API and real immediate write transactions, improving backup consistency and concurrent-write behavior when `better-sqlite3` is unavailable
- **feat(models):** add exact per-model `context_length`, `max_input_tokens`, and `max_output_tokens` overrides across model discovery and runtime enforcement, with automatic migration from the retired output-only `max_token` key ([#8908](https://github.com/diegosouzapw/OmniRoute/pull/8908)) — thanks @xz-dev
- **feat(providers):** native xAI Agent Tools passthrough on `/v1/responses` for `xai` / `xai-oauth` (`xao`) — forward `web_search` + `x_search` to `api.x.ai` instead of rewriting or rejecting them ([#8964](https://github.com/diegosouzapw/OmniRoute/issues/8964))
- **feat(providers): add UnoRouter provider** — UnoRouter is an OpenAI-compatible routing gateway supporting hundreds of models. It is now registered as an API-key provider. ([#8978](https://github.com/diegosouzapw/OmniRoute/issues/8978))
- **feat(providers):** make video_url passthrough configurable per provider/model via compat override ([#9248](https://github.com/diegosouzapw/OmniRoute/issues/9248)) — thanks @HellFiveOsborn
- **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))
- **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))
- **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))
- **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))
- **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))
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.