Compare commits

..

15 Commits

Author SHA1 Message Date
diegosouzapw
1c351cc6b9 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
2026-08-07 08:12:27 -03:00
diegosouzapw
9d353217f1 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
2026-08-07 08:01:24 -03:00
diegosouzapw
16904141f7 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
2026-08-07 07:39:40 -03:00
diegosouzapw
18717aa6b8 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
2026-08-07 07:02:12 -03:00
diegosouzapw
744ab48f1c 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
2026-08-07 06:12:43 -03:00
diegosouzapw
db7c066f87 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
2026-08-07 05:31:07 -03:00
diegosouzapw
eba6fb42d4 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
2026-08-07 05:02:11 -03:00
diegosouzapw
43f78b712d 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
2026-08-07 04:56:36 -03:00
diegosouzapw
54922fa1a6 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.
2026-08-07 04:40:04 -03:00
diegosouzapw
5d25d34700 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
2026-08-07 04:38:35 -03:00
diegosouzapw
db2c3a4753 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
2026-08-07 04:35:29 -03:00
diegosouzapw
b80987e4d4 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
2026-08-07 04:13:20 -03:00
diegosouzapw
e8936f53f4 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
2026-08-07 04:08:35 -03:00
diegosouzapw
e4f55c7d92 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
2026-08-07 03:35:29 -03:00
diegosouzapw
5dfc96ae54 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
2026-08-07 03:35:29 -03:00
524 changed files with 2214 additions and 25845 deletions

View File

@@ -345,11 +345,8 @@ ALLOW_API_KEY_REVEAL=false
# OMNIROUTE_CHAT_HEAVY_TOOL_COUNT=64
# Conservative string-size token estimate that classifies a request as heavyweight. Default 32000.
# OMNIROUTE_CHAT_HEAVY_ESTIMATED_TOKENS=32000
# Optional opt-in hard message-count cap; excess receives compact-required 413 before
# compression can run. Unset/0 (the default) means no history cap: heap growth is bounded
# by OMNIROUTE_CHAT_MAX_HEAVY_IN_FLIGHT and the heap-pressure shed instead. Set a positive
# value only on memory-constrained deployments that need a hard ceiling.
# OMNIROUTE_CHAT_HARD_MAX_MESSAGES=0
# Hard message-count cap; excess receives compact-required 413. Default 800.
# OMNIROUTE_CHAT_HARD_MAX_MESSAGES=800
# Hard cap (bytes) for a non-streaming upstream response buffered fully into memory
# (#5152). Past this the upstream reader is cancelled and the request fails fast
@@ -797,16 +794,6 @@ PROVIDER_LIMITS_SYNC_SPACING_MS=1500
# Disable the proactive recovery scheduler entirely (default: false).
# OMNIROUTE_DISABLE_CONNECTION_RECOVERY=false
# Proactive Claude warmup scheduler (#8848): fires a trivial request to opted-in
# OAuth connections on a cron schedule (America/Los_Angeles) so accounts do not
# hit the 5-hour sliding window cold. Off by default — set ENABLED=1 and flip
# per-connection flags in settings.claudeWarmup.connections to activate.
# Used by: src/lib/warmupScheduler.ts.
# OMNIROUTE_WARMUP_ENABLED=false
# OMNIROUTE_WARMUP_CRON="0 7 * * *"
# OMNIROUTE_WARMUP_CONCURRENCY=3
# OMNIROUTE_WARMUP_MODEL=
# Background job interval for budget reset checks (ms). Default: 600000 (10m).
# Used by: src/lib/jobs/budgetResetJob.ts. Floor: 10000.
#OMNIROUTE_BUDGET_RESET_JOB_INTERVAL_MS=600000
@@ -1540,15 +1527,6 @@ APP_LOG_TO_FILE=true
# ═══════════════════════════════════════════════════════════════════════════════
# 19. MODEL SYNC (Dev)
# ═══════════════════════════════════════════════════════════════════════════════
# Enable the models.dev capability sync. Default: false (opt-in only).
# Also settable from Dashboard > Settings > AI. This variable wins over that
# setting whenever it is set to anything non-empty, in either direction, so a
# deployment can pin the sync on or off without depending on database state
# surviving a rebuild. Leave it unset to let the dashboard toggle decide.
# On: 1, true, yes or on (any casing). Any other value is off.
# Used by: src/lib/modelsDevSync.ts
# MODELS_DEV_SYNC_ENABLED=false
# Development-time model catalog sync interval in seconds.
# Used by: src/lib/modelsDevSync.ts
# Default: 86400 (24 hours)
@@ -2492,10 +2470,10 @@ QUOTA_STORE_DRIVER=sqlite # sqlite | redis
# ═══════════════════════════════════════════════════════════════════════════════
# Optional add-on (feature flag RADAR_ENABLED, default off — see feature flag
# settings, not an env var) that overlays a signed, freshly-curated free-model
# catalog on top of the release baseline. All four variables below are optional
# and only needed to point the client at a self-hosted/forked feed or
# supporter-key flow instead of the default OmniRoute Radar service. Used by:
# src/lib/radar/sync.ts, src/lib/radar/pinnedKeys.ts, src/lib/radar/links.ts.
# catalog on top of the release baseline. Both variables below are optional and
# only needed to point the client at a self-hosted/forked feed instead of the
# default OmniRoute Radar feed. Used by: src/lib/radar/sync.ts,
# src/lib/radar/pinnedKeys.ts.
# Base URL of the Radar feed service. Overrides the built-in default so forks
# and self-hosters can point at their own signed feed.
@@ -2505,12 +2483,3 @@ QUOTA_STORE_DRIVER=sqlite # sqlite | redis
# signature, replacing the pinned default key. Required when self-hosting a
# feed signed with a different key pair.
# RADAR_FEED_PUBKEY=
# URL the dashboard's "I'm a contributor" button opens (GitHub OAuth
# supporter-key claim flow). No pricing/value lives in this repo — only the
# link.
# RADAR_CONTRIBUTOR_CLAIM_URL=https://radar.omniroute.online/auth/github
# URL the dashboard's "Support the project" button opens (payment/plans
# page). No pricing/value lives in this repo — only the link.
# RADAR_SUPPORTER_PLANS_URL=https://radar.omniroute.online/planos

View File

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

View File

@@ -137,13 +137,13 @@ jobs:
uses: docker/setup-buildx-action@v4
- name: Login to Docker Hub
uses: docker/login-action@v4.6.0
uses: docker/login-action@v4.5.2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v4.6.0
uses: docker/login-action@v4.5.2
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -237,13 +237,13 @@ jobs:
uses: docker/setup-buildx-action@v4
- name: Login to Docker Hub
uses: docker/login-action@v4.6.0
uses: docker/login-action@v4.5.2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v4.6.0
uses: docker/login-action@v4.5.2
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -372,7 +372,7 @@ jobs:
- name: Upload Trivy SARIF to Security tab
if: needs.prepare.outputs.version != 'main'
continue-on-error: true
uses: github/codeql-action/upload-sarif@v4.37.4
uses: github/codeql-action/upload-sarif@v4.37.3
with:
sarif_file: trivy-results.sarif
category: trivy-image

View File

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

4
.gitignore vendored
View File

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

View File

@@ -30,7 +30,7 @@ omniroute setup opencode --auth
# 3. Restart OpenCode — /models lists the full live catalog
```
The `--auth` flag runs `opencode auth login --provider opencode-omniroute` automatically.
The `--auth` flag runs `opencode auth login --provider 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 opencode-omniroute
opencode auth login --provider omniroute
# prompts for the OmniRoute API key, writes to ~/.local/share/opencode/auth.json
```
@@ -164,8 +164,8 @@ Then in `~/.config/opencode/opencode.json` reference each directory by absolute
Paths are relative to `~/.config/opencode/`. Each entry now resolves to a distinct module file, so OC loads them as two separate plugin instances. Authenticate each:
```sh
opencode auth login --provider opencode-omniroute
opencode auth login --provider opencode-omniroute-preprod
opencode auth login --provider omniroute
opencode auth login --provider omniroute-preprod
```
Each entry gets its own provider id, its own model picker entry, its own slot in `auth.json`, and its own TTL cache. Closures are isolated per plugin instance — no cross-talk.

View File

@@ -1033,11 +1033,7 @@ export const OmniRoutePlugin: Plugin = async (_input, options) => {
// Config hook: keep existing catalog shim, and register slash command
// templates that ask the agent to call the force-sync tool (OpenCode has no
// Pi-style registerCommand API; tools + command templates are the native path).
const baseConfigHook = createOmniRouteConfigHook(resolved, {
cache: sharedCache,
diskSnapshotReader: defaultDiskSnapshotReader,
diskSnapshotWriter: defaultDiskSnapshotWriter,
});
const baseConfigHook = createOmniRouteConfigHook(resolved, { cache: sharedCache });
const configWithSyncCommand = async (input: Config) => {
await baseConfigHook(input);
const cfg = input as Config & {
@@ -4407,11 +4403,11 @@ export function buildStaticProviderEntry(
entry.release_date = raw.release_date;
}
// #9175: OC's `getModel` looks the model up by BARE id — the part after
// the first `/` in the user's request — so a dict key with an embedded
// provider prefix (`<providerId>/<raw-id>`) is unreachable. Keys are the
// raw id verbatim; ids that already contain `/` (e.g. `cc/claude-opus-4-7`)
// keep it because the slash is part of the upstream model id itself.
// OC's static-catalog reader parses each key on `/` and rejects the
// entire provider block if ANY key resolves to a parsed providerID that
// has no corresponding provider block. So bare keys (no `/`) MUST be
// prefixed with the resolved providerId. Already-prefixed keys
// (e.g. `cc/claude-opus-4-7`) are left as-is to avoid double-prefixing.
models[raw.id] = entry;
}
@@ -4745,7 +4741,7 @@ export type OmniRouteDiskSnapshotWriter = (
export type OmniRouteDiskSnapshotReader = (
providerId: string,
identityFingerprint: string
) => Promise<(Omit<OmniRouteFetchCacheEntry, "expiresAt"> & { writtenAt?: number }) | undefined>;
) => Promise<Omit<OmniRouteFetchCacheEntry, "expiresAt"> | undefined>;
/**
* Bind a snapshot to the endpoint and effective credential tuple without
@@ -4828,36 +4824,15 @@ export const defaultDiskSnapshotReader: OmniRouteDiskSnapshotReader = async (
? parsed.rawCompressionCombos
: [],
rawConnections: Array.isArray(parsed.rawConnections) ? parsed.rawConnections : [],
writtenAt: typeof parsed.writtenAt === "number" ? parsed.writtenAt : undefined,
};
} catch {
return undefined;
}
};
/** No-op disk-cache pair — used by tests to avoid filesystem side effects.
* Also used as the default in createOmniRouteConfigHook so that tests
* that don't pass a diskSnapshotReader don't read real snapshot files
* from the user's ~/.local/share/opencode/plugins/ directory.
* The OmniRoutePlugin function passes the real defaultDiskSnapshotReader
* explicitly. */
export const noopDiskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
/** No-op disk-cache pair — used by tests to avoid filesystem side effects. */
export const noopDiskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
/**
* In-flight refresh guard: prevents concurrent refreshes for the same
* cacheKey. When a warm snapshot is served, the refresh runs detached; if
* a second hook invocation arrives before the refresh completes, it should
* piggyback on the in-flight promise rather than starting a second one.
* Cleared on settle so it doesn't leak.
*/
const _inflightRefresh: Map<string, Promise<void>> = new Map();
/** Reset the in-flight refresh guard (for test isolation). */
export function _resetInflightRefresh(): void {
_inflightRefresh.clear();
}
// ────────────────────────────────────────────────────────────────────────────
// Debug logging (features.debugLog)
// ────────────────────────────────────────────────────────────────────────────
@@ -5092,6 +5067,7 @@ export function createDebugLoggingFetch(
}
};
}
export const noopDiskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
export type OmniRouteReadAuthJson = () => Promise<AuthJsonShape | undefined | null>;
@@ -5194,8 +5170,8 @@ export function createOmniRouteConfigHook(
const compressionMetaFetcher =
deps.compressionMetaFetcher ?? defaultOmniRouteCompressionMetaFetcher;
const providersFetcher = deps.providersFetcher ?? defaultOmniRouteProvidersFetcher;
const diskSnapshotReader = deps.diskSnapshotReader ?? noopDiskSnapshotReader;
const diskSnapshotWriter = deps.diskSnapshotWriter ?? noopDiskSnapshotWriter;
const diskSnapshotReader = deps.diskSnapshotReader ?? defaultDiskSnapshotReader;
const diskSnapshotWriter = deps.diskSnapshotWriter ?? defaultDiskSnapshotWriter;
const now = deps.now ?? Date.now;
const cache: OmniRouteFetchCache = deps.cache ?? new Map();
const logger = deps.logger ?? console;
@@ -5290,12 +5266,12 @@ export function createOmniRouteConfigHook(
const t = now();
const cached = cache.get(cacheKey);
let rawModels: OmniRouteRawModelEntry[] = [];
let rawCombos: OmniRouteRawCombo[] = [];
let rawAutoCombos: OmniRouteRawAutoCombo[] = [];
let rawEnrichment: OmniRouteEnrichmentMap = new Map();
let rawCompressionCombos: OmniRouteCompressionCombo[] = [];
let rawConnections: OmniRouteProviderConnection[] = [];
let rawModels: OmniRouteRawModelEntry[];
let rawCombos: OmniRouteRawCombo[];
let rawAutoCombos: OmniRouteRawAutoCombo[];
let rawEnrichment: OmniRouteEnrichmentMap;
let rawCompressionCombos: OmniRouteCompressionCombo[];
let rawConnections: OmniRouteProviderConnection[];
if (cached && cached.expiresAt > t) {
rawModels = cached.rawModels;
@@ -5305,275 +5281,160 @@ export function createOmniRouteConfigHook(
rawCompressionCombos = cached.rawCompressionCombos;
rawConnections = cached.rawConnections;
} else {
// ─────────────────────────────────────────────────────────────────────
// Warm startup: read the disk snapshot before fetching so the provider
// registers immediately with the last-known-good catalog. The live
// fetch then refreshes in the background (detached) and updates the
// cache + snapshot. Gated by features.diskCache (default-on).
// ─────────────────────────────────────────────────────────────────────
let warmSnapshot: Omit<OmniRouteFetchCacheEntry, "expiresAt"> | undefined;
if (wantDiskCache) {
const snapshotResult = await diskSnapshotReader(resolved.providerId, snapshotFingerprint);
if (snapshotResult && snapshotResult.rawModels.length > 0) {
warmSnapshot = snapshotResult;
// Log snapshot age (accept any age — instant beats empty).
const age = (snapshotResult as { writtenAt?: number }).writtenAt;
const ageLabel = typeof age === "number" ? `${Math.round((Date.now() - age) / 3_600_000)}h` : "unknown";
// Fail-open fetcher errors: on /v1/models throw, fall back to empty
// catalog (still publish a stub block so OC has a complete-shape
// entry); on /api/combos throw, publish models-only. Disk-cache
// fallback below recovers the last-known-good catalog when the
// fetcher threw (network down / 403 / timeout) AND features.diskCache
// !== false. A 0-entry SUCCESS (fresh tenant) does NOT trigger
// disk fallback — that's a valid empty catalog.
let modelsFetchThrew = false;
try {
rawModels = await fetcher(baseURL, apiKey, 10_000);
} catch (err) {
logger.warn(
"[omniroute-plugin] config shim: /v1/models fetch failed; publishing stub provider entry",
err
);
rawModels = [];
modelsFetchThrew = true;
}
const modelsFetchOk = !modelsFetchThrew && rawModels.length > 0;
rawCombos = [];
try {
rawCombos = await combosFetcher(baseURL, managementReadToken, 10_000);
} catch (err) {
logger.warn(
"[omniroute-plugin] config shim: /api/combos fetch failed; publishing models-only static catalog",
err
);
}
rawAutoCombos = [];
if (wantAutoCombos) {
try {
rawAutoCombos = await autoCombosFetcher(baseURL, managementReadToken, 5_000);
} catch {
// Already handled inside the default fetcher
}
}
// Eagerly fetch enrichment so the static block can overlay human
// display names on raw model ids. On OC ≤1.15.5 the dynamic
// `provider.models` hook never fires in `serve` mode, so the static
// block IS what reaches `/provider` and the TUI model picker.
// Gated by `features.enrichment` (default-on). Soft-fail on error —
// we still publish a name-less catalog if /api/pricing/models is
// unreachable.
rawEnrichment = new Map();
if (wantEnrichment) {
try {
rawEnrichment = await enrichmentFetcher(baseURL, managementReadToken, 10_000);
} catch (err) {
logger.warn(
`[omniroute-plugin] config shim: warm startup from disk snapshot (${snapshotResult.rawModels.length} models, age ${ageLabel})`
"[omniroute-plugin] config shim: /api/pricing/models fetch failed; publishing raw-id static catalog",
err
);
}
}
// ─────────────────────────────────────────────────────────────────────
// Parallel refresh: all six fetchers run concurrently via
// Promise.allSettled. Each wrapper never rejects (catches internally)
// so partial failure is tolerated — same soft-fail semantics as the
// old sequential chain, but ~6x faster.
// ─────────────────────────────────────────────────────────────────────
const doRefresh = async (): Promise<void> => {
let modelsFetchThrew = false;
let localRawModels: OmniRouteRawModelEntry[] = [];
let localRawCombos: OmniRouteRawCombo[] = [];
let localRawAutoCombos: OmniRouteRawAutoCombo[] = [];
let localRawEnrichment: OmniRouteEnrichmentMap = new Map();
let localRawCompressionCombos: OmniRouteCompressionCombo[] = [];
let localRawConnections: OmniRouteProviderConnection[] = [];
// Each wrapper keeps the existing try/catch, default value, and
// exact warn message so per-endpoint fallbacks are preserved.
const doModels = async (): Promise<void> => {
try {
localRawModels = await fetcher(baseURL, apiKey, 10_000);
} catch (err) {
logger.warn(
"[omniroute-plugin] config shim: /v1/models fetch failed; publishing stub provider entry",
err
);
localRawModels = [];
modelsFetchThrew = true;
}
};
const doCombos = async (): Promise<void> => {
try {
localRawCombos = await combosFetcher(baseURL, managementReadToken, 10_000);
} catch (err) {
logger.warn(
"[omniroute-plugin] config shim: /api/combos fetch failed; publishing models-only static catalog",
err
);
}
};
const doAutoCombos = async (): Promise<void> => {
if (!wantAutoCombos) return;
try {
localRawAutoCombos = await autoCombosFetcher(baseURL, managementReadToken, 5_000);
} catch {
// Already handled inside the default fetcher
}
};
const doEnrichment = async (): Promise<void> => {
if (!wantEnrichment) return;
try {
localRawEnrichment = await enrichmentFetcher(baseURL, managementReadToken, 10_000);
} catch (err) {
logger.warn(
"[omniroute-plugin] config shim: /api/pricing/models fetch failed; publishing raw-id static catalog",
err
);
}
};
const doCompression = async (): Promise<void> => {
if (!wantCompressionMeta) return;
try {
localRawCompressionCombos = await compressionMetaFetcher(baseURL, managementReadToken, 10_000);
} catch (err) {
logger.warn(
"[omniroute-plugin] config shim: /api/context/combos fetch failed; publishing combos without compression suffix",
err
);
}
};
const doConnections = async (): Promise<void> => {
if (!wantUsableOnly) return;
try {
localRawConnections = await providersFetcher(baseURL, managementReadToken, 10_000);
} catch (err) {
logger.warn(
"[omniroute-plugin] config shim: /api/providers fetch failed; usableOnly filter disabled for this refresh",
err
);
}
};
await Promise.allSettled([
doModels(),
doCombos(),
doAutoCombos(),
doEnrichment(),
doCompression(),
doConnections(),
]);
const modelsFetchOk = !modelsFetchThrew && localRawModels.length > 0;
// Disk-cache fallback (cold first run, no warm snapshot): when the
// live fetch returned no models AND features.diskCache !== false,
// hydrate from the last-known-good snapshot so OC still surfaces a
// usable catalog (e.g. IP whitelist drop, offline laptop).
if (modelsFetchThrew && wantDiskCache && !warmSnapshot) {
const snapshot = await diskSnapshotReader(resolved.providerId, snapshotFingerprint);
if (snapshot && snapshot.rawModels.length > 0) {
logger.warn(
`[omniroute-plugin] config shim: /v1/models unreachable; using stale disk cache (${snapshot.rawModels.length} models)`
);
localRawModels = snapshot.rawModels;
localRawCombos = snapshot.rawCombos;
localRawAutoCombos = snapshot.rawAutoCombos ?? [];
localRawEnrichment = snapshot.rawEnrichment;
localRawCompressionCombos = snapshot.rawCompressionCombos;
localRawConnections = snapshot.rawConnections;
}
// Compression-metadata fetch — opt-in via features.compressionMetadata.
// When on, the default pipeline is appended to every combo `name` so
// the TUI picker advertises which compression a combo applies.
rawCompressionCombos = [];
if (wantCompressionMeta) {
try {
rawCompressionCombos = await compressionMetaFetcher(baseURL, managementReadToken, 10_000);
} catch (err) {
logger.warn(
"[omniroute-plugin] config shim: /api/context/combos fetch failed; publishing combos without compression suffix",
err
);
}
}
// Cache even partial results — a subsequent provider-hook call should
// not re-burn the timeout window on the same broken endpoint.
cache.set(cacheKey, {
rawModels: localRawModels,
rawCombos: localRawCombos,
rawAutoCombos: localRawAutoCombos,
rawEnrichment: localRawEnrichment,
rawCompressionCombos: localRawCompressionCombos,
rawConnections: localRawConnections,
expiresAt: now() + resolved.modelCacheTtl,
// Provider-connections fetch — opt-in via features.usableOnly. When
// on, the static catalog filters out models/combos whose canonical
// provider has no active connection. Soft-fail (empty list) disables
// the filter for this refresh, never hiding the whole catalog.
rawConnections = [];
if (wantUsableOnly) {
try {
rawConnections = await providersFetcher(baseURL, managementReadToken, 10_000);
} catch (err) {
logger.warn(
"[omniroute-plugin] config shim: /api/providers fetch failed; usableOnly filter disabled for this refresh",
err
);
}
}
// Disk-cache fallback: when the live fetch returned no models AND
// features.diskCache !== false, hydrate from the last-known-good
// snapshot so OC still surfaces a usable catalog (e.g. IP whitelist
// drop, offline laptop). The snapshot is whatever we last wrote on
// a healthy refresh; staleness is bounded only by how recently the
// user was online.
if (modelsFetchThrew && wantDiskCache) {
const snapshot = await diskSnapshotReader(resolved.providerId, snapshotFingerprint);
if (snapshot && snapshot.rawModels.length > 0) {
logger.warn(
`[omniroute-plugin] config shim: /v1/models unreachable; using stale disk cache (${snapshot.rawModels.length} models)`
);
rawModels = snapshot.rawModels;
rawCombos = snapshot.rawCombos;
rawAutoCombos = snapshot.rawAutoCombos ?? [];
rawEnrichment = snapshot.rawEnrichment;
rawCompressionCombos = snapshot.rawCompressionCombos;
rawConnections = snapshot.rawConnections;
}
}
// Cache even partial results — a subsequent provider-hook call should
// not re-burn the timeout window on the same broken endpoint.
cache.set(cacheKey, {
rawModels,
rawCombos,
rawAutoCombos,
rawEnrichment,
rawCompressionCombos,
rawConnections,
expiresAt: t + resolved.modelCacheTtl,
});
// Startup diagnostics (file-based) — fires at startup via config hook
if (resolved.features?.startupDebug === true) {
await writeStartupDiagnostics({
providerId: resolved.providerId,
baseURL,
modelCount: rawModels.length,
comboCount: rawCombos.length,
enrichmentSize: rawEnrichment.size,
autoComboCount: rawAutoCombos.length,
enrichment: rawEnrichment,
autoCombos: rawAutoCombos,
features: resolved.features,
});
}
// Startup diagnostics (file-based) — fires at startup via config hook
if (resolved.features?.startupDebug === true) {
await writeStartupDiagnostics({
providerId: resolved.providerId,
baseURL,
modelCount: localRawModels.length,
comboCount: localRawCombos.length,
enrichmentSize: localRawEnrichment.size,
autoComboCount: localRawAutoCombos.length,
enrichment: localRawEnrichment,
autoCombos: localRawAutoCombos,
features: resolved.features,
});
}
// Disk-cache write: persist the last successful (or any non-empty)
// catalog so a subsequent cold start with a failed fetch can recover.
// Best-effort; soft-fail keeps us moving when the data dir isn't
// writable (e.g. read-only container). A failed refresh never
// overwrites the snapshot (modelsFetchOk gate).
if (modelsFetchOk && wantDiskCache) {
await diskSnapshotWriter(
resolved.providerId,
{
rawModels: localRawModels,
rawCombos: localRawCombos,
rawAutoCombos: localRawAutoCombos,
rawEnrichment: localRawEnrichment,
rawCompressionCombos: localRawCompressionCombos,
rawConnections: localRawConnections,
},
snapshotFingerprint
);
}
// Re-publish a fresh block via the shared cache so OC >=1.14.49's
// dynamic provider hook picks it up from the cache. When the models
// fetch threw and a warm snapshot was served, keep the warm block
// (no downgrade to stub).
if (modelsFetchOk || !warmSnapshot) {
const freshBlock = buildStaticProviderEntry(
localRawModels,
localRawCombos,
resolved,
baseURL,
apiKey,
localRawEnrichment,
localRawCompressionCombos,
localRawConnections,
localRawAutoCombos
);
const inputWithProvider2 = input as { provider?: Record<string, unknown> };
if (inputWithProvider2.provider) {
inputWithProvider2.provider[resolved.providerId] = freshBlock;
}
}
};
if (warmSnapshot) {
// Warm startup: publish the snapshot block immediately, then run
// the refresh detached (never a floating unhandled rejection).
rawModels = warmSnapshot.rawModels;
rawCombos = warmSnapshot.rawCombos;
rawAutoCombos = warmSnapshot.rawAutoCombos ?? [];
rawEnrichment = warmSnapshot.rawEnrichment;
rawCompressionCombos = warmSnapshot.rawCompressionCombos;
rawConnections = warmSnapshot.rawConnections;
// In-flight guard: if a refresh is already running for this
// cacheKey, piggyback on it instead of starting a second one.
const existing = _inflightRefresh.get(cacheKey);
if (existing) {
// Another refresh is in-flight — don't start a second one.
// The existing refresh will update the cache when it completes.
} else {
const refreshP = doRefresh()
.catch((err: unknown) => {
logger.warn("[omniroute-plugin] config shim: background refresh failed", err);
})
.finally(() => {
_inflightRefresh.delete(cacheKey);
});
_inflightRefresh.set(cacheKey, refreshP);
}
} else {
// Cold first run (no warm snapshot): await the refresh so the
// first publish is always correct. In-flight guard still applies.
const existing = _inflightRefresh.get(cacheKey);
if (existing) {
await existing;
// After the in-flight refresh completes, the cache has the data.
const fresh = cache.get(cacheKey);
if (fresh) {
rawModels = fresh.rawModels;
rawCombos = fresh.rawCombos;
rawAutoCombos = fresh.rawAutoCombos;
rawEnrichment = fresh.rawEnrichment;
rawCompressionCombos = fresh.rawCompressionCombos;
rawConnections = fresh.rawConnections;
}
} else {
const refreshP = doRefresh()
.catch((err: unknown) => {
logger.warn("[omniroute-plugin] config shim: refresh failed", err);
})
.finally(() => {
_inflightRefresh.delete(cacheKey);
});
_inflightRefresh.set(cacheKey, refreshP);
await refreshP;
// After the refresh, the cache has the data.
const fresh = cache.get(cacheKey);
if (fresh) {
rawModels = fresh.rawModels;
rawCombos = fresh.rawCombos;
rawAutoCombos = fresh.rawAutoCombos;
rawEnrichment = fresh.rawEnrichment;
rawCompressionCombos = fresh.rawCompressionCombos;
rawConnections = fresh.rawConnections;
}
}
// Disk-cache write: persist the last successful (or any non-empty)
// catalog so a subsequent cold start with a failed fetch can recover.
// Best-effort; soft-fail keeps us moving when the data dir isn't
// writable (e.g. read-only container).
if (modelsFetchOk && wantDiskCache) {
await diskSnapshotWriter(
resolved.providerId,
{
rawModels,
rawCombos,
rawAutoCombos,
rawEnrichment,
rawCompressionCombos,
rawConnections,
},
snapshotFingerprint
);
}
}

View File

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

View File

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

View File

@@ -1,827 +0,0 @@
/**
* Warm-startup + parallel-refresh tests for the opencode-plugin config shim.
*
* Covers `createOmniRouteConfigHook(opts, deps)`:
* - (a) Warm startup: cache miss + matching snapshot → provider block
* populated from snapshot data (not live fetch data).
* - (b) Fingerprint mismatch: reader returns undefined → no warm publish,
* falls through to awaited fetch (cold-start behavior).
* - (c) Successful parallel refresh: all fetchers resolve → cache updated,
* disk snapshot written.
* - (d) Failed refresh keeps the snapshot: warm-served + models fetcher
* rejects → no disk overwrite, block stays at warm-snapshot shape.
* - (e) Parallelism: all six fetchers start concurrently (not sequential).
* - (f) Soft-fail parity under Promise.allSettled: per-endpoint
* fallbacks + logger.warn breadcrumbs preserved.
* - (g) No double-refresh: concurrent hook invocations on the same cacheKey
* trigger only one refresh (in-flight guard).
* - (h) features.diskCache: false disables the warm read entirely.
*
* Mocking strategy: every dependency is DI-injected at hook construction
* (same pattern as config-shim.test.ts). No global monkey-patching.
*/
import test from "node:test";
import assert from "node:assert/strict";
import type { Config } from "@opencode-ai/plugin";
import {
createOmniRouteConfigHook,
resolveOmniRoutePluginOptions,
_resetInflightRefresh,
type OmniRouteAutoCombosFetcher,
type OmniRouteCombosFetcher,
type OmniRouteCompressionMetaFetcher,
type OmniRouteEnrichmentEntry,
type OmniRouteEnrichmentFetcher,
type OmniRouteEnrichmentMap,
type OmniRouteFetchCache,
type OmniRouteModelsFetcher,
type OmniRouteProviderConnection,
type OmniRouteProvidersFetcher,
type OmniRouteRawAutoCombo,
type OmniRouteRawCombo,
type OmniRouteRawModelEntry,
type OmniRouteReadAuthJson,
type OmniRouteStaticProviderEntry,
type OmniRouteDiskSnapshotReader,
type OmniRouteDiskSnapshotWriter,
type OmniRouteCompressionCombo,
} from "../src/index.js";
// ────────────────────────────────────────────────────────────────────────────
// Test isolation: reset the module-level in-flight refresh guard between
// tests so a detached refresh from a previous test doesn't leak into the
// next one (same cacheKey, different cache instance).
// ────────────────────────────────────────────────────────────────────────────
test.beforeEach(() => {
_resetInflightRefresh();
});
// ────────────────────────────────────────────────────────────────────────────
// Fixtures
// ────────────────────────────────────────────────────────────────────────────
const MODEL_CLAUDE: OmniRouteRawModelEntry = {
id: "claude-sonnet-4-6",
capabilities: {
tool_calling: true,
reasoning: true,
vision: true,
thinking: false,
temperature: true,
},
context_length: 200_000,
max_output_tokens: 64_000,
max_input_tokens: 180_000,
input_modalities: ["text", "image"],
output_modalities: ["text"],
};
const MODEL_GEMINI: OmniRouteRawModelEntry = {
id: "gemini-3-flash",
capabilities: { tool_calling: true, reasoning: false, vision: true, thinking: false },
context_length: 1_000_000,
max_output_tokens: 8_192,
input_modalities: ["text", "image"],
output_modalities: ["text"],
};
const COMBO_CLAUDE_TIER: OmniRouteRawCombo = {
id: "combo-claude-tier",
name: "Claude Tier",
models: [
{ id: "s1", kind: "model", model: "claude-sonnet-4-6", weight: 100 },
{ id: "s2", kind: "model", model: "gemini-3-flash", weight: 50 },
],
};
const AUTO_COMBO: OmniRouteRawAutoCombo = {
id: "auto",
name: "Auto",
};
const COMPRESSION_COMBO: OmniRouteCompressionCombo = {
id: "ctx-combo-1",
name: "Context Combo",
pipeline: "gzip",
};
const CONNECTION_CLAUDE: OmniRouteProviderConnection = {
id: "c1",
provider: "claude",
isActive: true,
testStatus: "active",
};
// ────────────────────────────────────────────────────────────────────────────
// DI stub helpers
// ────────────────────────────────────────────────────────────────────────────
function stubReadAuthJson(
value: Record<string, unknown> | undefined | null
): OmniRouteReadAuthJson {
return async () => value as never;
}
function immediateFetcher<T extends (...args: unknown[]) => Promise<unknown>>(
payload: ReturnType<T> extends Promise<infer U> ? U : never
): T & { callCount: () => number; startedAt: () => number | undefined } {
let n = 0;
let start: number | undefined;
const f = async (..._args: unknown[]) => {
start = Date.now();
n++;
return payload;
};
return Object.assign(f as T, { callCount: () => n, startedAt: () => start });
}
function throwingFetcher<T extends (...args: unknown[]) => Promise<unknown>>(
msg = "ECONNREFUSED"
): T & { callCount: () => number } {
let n = 0;
const f = async (..._args: unknown[]) => {
n++;
throw new Error(msg);
};
return Object.assign(f as T, { callCount: () => n });
}
interface WarnCapture {
warn: (...args: unknown[]) => void;
entries: unknown[][];
}
function captureWarn(): WarnCapture {
const entries: unknown[][] = [];
return {
warn: (...args: unknown[]) => {
entries.push(args);
},
entries,
};
}
function makeInput(initialProvider: Record<string, unknown> = {}): Config {
return { provider: initialProvider } as unknown as Config;
}
/** Build a valid auth.json stub for the default providerId. */
function authStub() {
return stubReadAuthJson({
"opencode-omniroute": {
type: "api",
key: "sk-test",
baseURL: "https://or.example.com/v1",
},
});
}
// ────────────────────────────────────────────────────────────────────────────
// (a) Warm startup: cache miss + matching snapshot → provider block populated
// from snapshot data (not live fetch data)
// ────────────────────────────────────────────────────────────────────────────
test("warm-startup: snapshot data used when snapshot is present", async () => {
// Live fetch returns MODEL_CLAUDE, but snapshot has MODEL_GEMINI.
// With warm startup, the block should contain the snapshot data.
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
const autoCombosFetcher = immediateFetcher<OmniRouteAutoCombosFetcher>([]);
const enrichmentFetcher = immediateFetcher<OmniRouteEnrichmentFetcher>(new Map());
const compressionMetaFetcher = immediateFetcher<OmniRouteCompressionMetaFetcher>([]);
const providersFetcher = immediateFetcher<OmniRouteProvidersFetcher>([]);
const logger = captureWarn();
const snapshot: Omit<import("../src/index.js").OmniRouteFetchCacheEntry, "expiresAt"> = {
rawModels: [MODEL_GEMINI],
rawCombos: [],
rawAutoCombos: [],
rawEnrichment: new Map(),
rawCompressionCombos: [],
rawConnections: [],
};
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => snapshot;
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
const hook = createOmniRouteConfigHook(
{ providerId: "omniroute" },
{
readAuthJson: authStub(),
fetcher,
combosFetcher,
autoCombosFetcher,
enrichmentFetcher,
compressionMetaFetcher,
providersFetcher,
diskSnapshotReader,
diskSnapshotWriter,
logger,
}
);
const input = makeInput();
await hook(input);
const provider = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider;
const entry = provider["opencode-omniroute"];
assert.ok(entry, "provider entry published");
// With warm startup, the block should contain the snapshot data (GEMINI),
// not the live fetch data (CLAUDE). This is the key assertion: the warm
// snapshot is served first, and the live refresh updates the cache in the
// background. On the next hook invocation, the cache will have the fresh data.
const hasGemini = entry.models["opencode-omniroute/gemini-3-flash"] !== undefined;
const hasClaude = entry.models["opencode-omniroute/claude-sonnet-4-6"] !== undefined;
assert.ok(
hasGemini || hasClaude,
"provider block has at least one model"
);
// The warm-startup breadcrumb should be emitted.
assert.ok(
logger.entries.some((e) =>
String(e[0]).includes("warm startup from disk snapshot")
),
"warm-startup breadcrumb emitted"
);
});
// ────────────────────────────────────────────────────────────────────────────
// (b) Fingerprint mismatch: reader returns undefined → no warm publish,
// falls through to awaited fetch
// ────────────────────────────────────────────────────────────────────────────
test("warm-startup: fingerprint mismatch → no warm publish, awaited fetch", async () => {
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
const logger = captureWarn();
// Reader returns undefined → fingerprint mismatch or missing snapshot.
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
const hook = createOmniRouteConfigHook(
{ providerId: "omniroute" },
{
readAuthJson: authStub(),
fetcher,
combosFetcher,
diskSnapshotReader,
diskSnapshotWriter,
logger,
}
);
const input = makeInput();
await hook(input);
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
assert.ok(entry, "provider entry published from live fetch");
// Live fetch data, not snapshot data.
assert.ok(
entry.models["opencode-omniroute/claude-sonnet-4-6"],
"live fetch model present"
);
assert.equal(fetcher.callCount(), 1, "fetcher was called (awaited cold path)");
// No warm-startup breadcrumb when no snapshot.
assert.ok(
!logger.entries.some((e) =>
String(e[0]).includes("warm startup from disk snapshot")
),
"no warm-startup breadcrumb when no snapshot"
);
});
// ────────────────────────────────────────────────────────────────────────────
// (c) Successful parallel refresh: all fetchers resolve → cache updated,
// disk snapshot written, block re-published with fresh data
// ────────────────────────────────────────────────────────────────────────────
test("warm-startup: parallel refresh updates cache + writes snapshot", async () => {
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([COMBO_CLAUDE_TIER]);
const autoCombosFetcher = immediateFetcher<OmniRouteAutoCombosFetcher>([AUTO_COMBO]);
const enrichmentFetcher = immediateFetcher<OmniRouteEnrichmentFetcher>(
new Map<string, OmniRouteEnrichmentEntry>([
["claude-sonnet-4-6", { name: "Claude Sonnet 4.6" }],
])
);
const compressionMetaFetcher = immediateFetcher<OmniRouteCompressionMetaFetcher>([
COMPRESSION_COMBO,
]);
const providersFetcher = immediateFetcher<OmniRouteProvidersFetcher>([CONNECTION_CLAUDE]);
const logger = captureWarn();
const snapshot: Omit<import("../src/index.js").OmniRouteFetchCacheEntry, "expiresAt"> = {
rawModels: [MODEL_GEMINI],
rawCombos: [],
rawAutoCombos: [],
rawEnrichment: new Map(),
rawCompressionCombos: [],
rawConnections: [],
};
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => snapshot;
let snapshotWrites = 0;
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {
snapshotWrites++;
};
const sharedCache: OmniRouteFetchCache = new Map();
const hook = createOmniRouteConfigHook(
{ providerId: "omniroute", modelCacheTtl: 60_000 },
{
readAuthJson: authStub(),
fetcher,
combosFetcher,
autoCombosFetcher,
enrichmentFetcher,
compressionMetaFetcher,
providersFetcher,
diskSnapshotReader,
diskSnapshotWriter,
cache: sharedCache,
logger,
}
);
const input = makeInput();
await hook(input);
// Warm block should have been published.
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
assert.ok(entry, "warm provider entry published");
// Give detached refresh time to complete.
await new Promise((r) => setTimeout(r, 100));
// After parallel refresh, the cache should have the fresh data.
const cacheKey = Array.from(sharedCache.keys())[0];
assert.ok(cacheKey, "cache entry created");
const cached = sharedCache.get(cacheKey)!;
assert.ok(cached.expiresAt > 0, "cache entry has expiresAt");
// Fresh data from the live fetchers (not the stale snapshot).
assert.equal(cached.rawModels.length, 1, "cache has fresh models");
assert.equal(cached.rawModels[0].id, "claude-sonnet-4-6", "cache has correct model");
// Disk snapshot should have been written.
assert.equal(snapshotWrites, 1, "disk snapshot written after successful refresh");
});
// ────────────────────────────────────────────────────────────────────────────
// (d) Failed refresh keeps the snapshot: warm-served + models fetcher
// rejects → no disk overwrite, block stays at warm-snapshot shape
// ────────────────────────────────────────────────────────────────────────────
test("warm-startup: failed refresh keeps the snapshot, no disk overwrite", async () => {
const fetcher = throwingFetcher<OmniRouteModelsFetcher>();
const combosFetcher = throwingFetcher<OmniRouteCombosFetcher>();
const logger = captureWarn();
const snapshot: Omit<import("../src/index.js").OmniRouteFetchCacheEntry, "expiresAt"> = {
rawModels: [MODEL_GEMINI],
rawCombos: [COMBO_CLAUDE_TIER],
rawAutoCombos: [],
rawEnrichment: new Map(),
rawCompressionCombos: [],
rawConnections: [],
};
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => snapshot;
let snapshotWrites = 0;
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {
snapshotWrites++;
};
const hook = createOmniRouteConfigHook(
{ providerId: "omniroute" },
{
readAuthJson: authStub(),
fetcher,
combosFetcher,
diskSnapshotReader,
diskSnapshotWriter,
logger,
}
);
const input = makeInput();
await hook(input);
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
assert.ok(entry, "warm provider entry published");
// The block should contain the warm snapshot data (gemini), not be
// downgraded to a stub.
assert.ok(
entry.models["opencode-omniroute/gemini-3-flash"],
"warm snapshot model preserved (not downgraded to stub)"
);
// Give detached refresh time to complete.
await new Promise((r) => setTimeout(r, 100));
// No disk write on failed refresh.
assert.equal(snapshotWrites, 0, "no disk snapshot written when models fetch failed");
});
// ────────────────────────────────────────────────────────────────────────────
// (e) Parallelism: all six fetchers start concurrently (not sequential)
// ────────────────────────────────────────────────────────────────────────────
test("warm-startup: all fetchers start concurrently (parallel fan-out)", async () => {
const startTimes: number[] = [];
const barrier = new Promise<void>((r) => {
setTimeout(r, 30);
});
function instrumentedFetcher<T extends (...args: unknown[]) => Promise<unknown>>(
payload: ReturnType<T> extends Promise<infer U> ? U : never
): T & { callCount: () => number } {
let n = 0;
const f = async (..._args: unknown[]) => {
startTimes.push(Date.now());
n++;
await barrier;
return payload;
};
return Object.assign(f as T, { callCount: () => n });
}
const fetcher = instrumentedFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
const combosFetcher = instrumentedFetcher<OmniRouteCombosFetcher>([]);
const autoCombosFetcher = instrumentedFetcher<OmniRouteAutoCombosFetcher>([]);
const enrichmentFetcher = instrumentedFetcher<OmniRouteEnrichmentFetcher>(new Map());
const compressionMetaFetcher = instrumentedFetcher<OmniRouteCompressionMetaFetcher>([]);
const providersFetcher = instrumentedFetcher<OmniRouteProvidersFetcher>([]);
const logger = captureWarn();
// No snapshot → cold path (awaited). All fetchers must still start
// concurrently.
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
const hook = createOmniRouteConfigHook(
{ providerId: "omniroute", features: { enrichment: true, compressionMetadata: true, usableOnly: true } },
{
readAuthJson: authStub(),
fetcher,
combosFetcher,
autoCombosFetcher,
enrichmentFetcher,
compressionMetaFetcher,
providersFetcher,
diskSnapshotReader,
diskSnapshotWriter,
logger,
}
);
const input = makeInput();
await hook(input);
// All fetchers should have been called.
assert.equal(fetcher.callCount(), 1, "models fetcher called");
assert.equal(combosFetcher.callCount(), 1, "combos fetcher called");
assert.equal(autoCombosFetcher.callCount(), 1, "autoCombos fetcher called");
assert.equal(enrichmentFetcher.callCount(), 1, "enrichment fetcher called");
assert.equal(compressionMetaFetcher.callCount(), 1, "compressionMeta fetcher called");
assert.equal(providersFetcher.callCount(), 1, "providers fetcher called");
// All start times should be within 20ms of each other (parallel fan-out),
// NOT sequential (which would show ~30ms gaps between each).
assert.ok(startTimes.length >= 6, "all 6 fetchers started");
const minStart = Math.min(...startTimes);
const maxStart = Math.max(...startTimes);
assert.ok(
maxStart - minStart < 20,
`all fetchers started within 20ms (spread: ${maxStart - minStart}ms) — parallel fan-out confirmed`
);
});
// ────────────────────────────────────────────────────────────────────────────
// (f) Soft-fail parity under Promise.allSettled: per-endpoint fallbacks +
// logger.warn breadcrumbs preserved
// ────────────────────────────────────────────────────────────────────────────
test("warm-startup: combos reject → models-only catalog with warn", async () => {
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
const combosFetcher = throwingFetcher<OmniRouteCombosFetcher>("403 Forbidden");
const logger = captureWarn();
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
const hook = createOmniRouteConfigHook(
{ providerId: "omniroute" },
{
readAuthJson: authStub(),
fetcher,
combosFetcher,
diskSnapshotReader,
diskSnapshotWriter,
logger,
}
);
const input = makeInput();
await hook(input);
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
assert.ok(entry, "provider entry published");
assert.ok(
entry.models["opencode-omniroute/claude-sonnet-4-6"],
"models-only catalog (no combos)"
);
assert.ok(
logger.entries.some((e) => String(e[0]).includes("/api/combos fetch failed")),
"combos-fetch breadcrumb emitted"
);
});
test("warm-startup: enrichment rejects → raw-id catalog with warn", async () => {
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
const enrichmentFetcher = throwingFetcher<OmniRouteEnrichmentFetcher>("ETIMEDOUT");
const logger = captureWarn();
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
const hook = createOmniRouteConfigHook(
{ providerId: "omniroute" },
{
readAuthJson: authStub(),
fetcher,
combosFetcher,
enrichmentFetcher,
diskSnapshotReader,
diskSnapshotWriter,
logger,
}
);
const input = makeInput();
await hook(input);
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
assert.ok(entry, "provider entry published");
assert.equal(
entry.models["opencode-omniroute/claude-sonnet-4-6"].name,
"claude-sonnet-4-6",
"raw id retained (no enrichment)"
);
assert.ok(
logger.entries.some((e) => String(e[0]).includes("/api/pricing/models fetch failed")),
"enrichment-fetch breadcrumb emitted"
);
});
test("warm-startup: providers reject → usableOnly filter disabled with warn", async () => {
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
const providersFetcher = throwingFetcher<OmniRouteProvidersFetcher>("ETIMEDOUT");
const logger = captureWarn();
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
const hook = createOmniRouteConfigHook(
{ providerId: "omniroute", features: { usableOnly: true } },
{
readAuthJson: authStub(),
fetcher,
combosFetcher,
providersFetcher,
diskSnapshotReader,
diskSnapshotWriter,
logger,
}
);
const input = makeInput();
await hook(input);
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
assert.ok(entry, "provider entry published");
// Soft-fail: model kept (filter disabled).
assert.ok(
entry.models["opencode-omniroute/claude-sonnet-4-6"],
"model kept (usableOnly filter disabled)"
);
assert.ok(
logger.entries.some((e) => String(e[0]).includes("/api/providers fetch failed")),
"providers-fetch breadcrumb emitted"
);
});
// ────────────────────────────────────────────────────────────────────────────
// (g) No double-refresh: concurrent hook invocations on the same cacheKey
// trigger only one refresh (in-flight guard)
// ────────────────────────────────────────────────────────────────────────────
test("warm-startup: concurrent hook invocations dedupe refresh", async () => {
let fetchCount = 0;
const slowResolve = new Promise<void>((r) => {
setTimeout(r, 100);
});
const fetcher: OmniRouteModelsFetcher = async () => {
fetchCount++;
await slowResolve;
return [MODEL_CLAUDE];
};
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
const logger = captureWarn();
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
const sharedCache: OmniRouteFetchCache = new Map();
const hook = createOmniRouteConfigHook(
{ providerId: "omniroute", modelCacheTtl: 60_000 },
{
readAuthJson: authStub(),
fetcher,
combosFetcher,
diskSnapshotReader,
diskSnapshotWriter,
cache: sharedCache,
logger,
}
);
// Fire two concurrent hook invocations on the same cache.
const inputA = makeInput();
const inputB = makeInput();
await Promise.all([hook(inputA), hook(inputB)]);
// Both should have published, but the refresh should only run once.
assert.equal(
fetchCount,
1,
"models fetcher called only once across concurrent invocations (in-flight guard)"
);
});
// ────────────────────────────────────────────────────────────────────────────
// (h) features.diskCache: false disables the warm read entirely
// ────────────────────────────────────────────────────────────────────────────
test("warm-startup: diskCache=false disables warm read, falls through to awaited fetch", async () => {
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
const logger = captureWarn();
let readerCalled = false;
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => {
readerCalled = true;
return {
rawModels: [MODEL_GEMINI],
rawCombos: [],
rawAutoCombos: [],
rawEnrichment: new Map(),
rawCompressionCombos: [],
rawConnections: [],
};
};
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
const hook = createOmniRouteConfigHook(
{ providerId: "omniroute", features: { diskCache: false } },
{
readAuthJson: authStub(),
fetcher,
combosFetcher,
diskSnapshotReader,
diskSnapshotWriter,
logger,
}
);
const input = makeInput();
await hook(input);
assert.equal(readerCalled, false, "disk snapshot reader NOT called when diskCache=false");
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
assert.ok(entry, "provider entry published from live fetch");
assert.ok(
entry.models["opencode-omniroute/claude-sonnet-4-6"],
"live fetch model present (not snapshot)"
);
});
// ────────────────────────────────────────────────────────────────────────────
// Warm startup: snapshot age logged
// ────────────────────────────────────────────────────────────────────────────
test("warm-startup: snapshot age is logged when warm-starting from disk", async () => {
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
const logger = captureWarn();
const snapshot: Omit<import("../src/index.js").OmniRouteFetchCacheEntry, "expiresAt"> & {
writtenAt?: number;
} = {
rawModels: [MODEL_GEMINI],
rawCombos: [],
rawAutoCombos: [],
rawEnrichment: new Map(),
rawCompressionCombos: [],
rawConnections: [],
writtenAt: Date.now() - 3_600_000, // 1 hour ago
};
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => snapshot;
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
const hook = createOmniRouteConfigHook(
{ providerId: "omniroute" },
{
readAuthJson: authStub(),
fetcher,
combosFetcher,
diskSnapshotReader,
diskSnapshotWriter,
logger,
}
);
const input = makeInput();
await hook(input);
// The log should mention "warm startup from disk snapshot".
assert.ok(
logger.entries.some((e) =>
String(e[0]).includes("warm startup from disk snapshot")
),
"warm-startup breadcrumb emitted"
);
});
// ────────────────────────────────────────────────────────────────────────────
// Warm startup: empty snapshot (rawModels.length === 0) is skipped
// ────────────────────────────────────────────────────────────────────────────
test("warm-startup: empty snapshot (rawModels.length=0) is skipped, falls through to fetch", async () => {
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
const logger = captureWarn();
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => ({
rawModels: [],
rawCombos: [],
rawAutoCombos: [],
rawEnrichment: new Map(),
rawCompressionCombos: [],
rawConnections: [],
});
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
const hook = createOmniRouteConfigHook(
{ providerId: "omniroute" },
{
readAuthJson: authStub(),
fetcher,
combosFetcher,
diskSnapshotReader,
diskSnapshotWriter,
logger,
}
);
const input = makeInput();
await hook(input);
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
assert.ok(entry, "provider entry published from live fetch");
// Live data, not empty snapshot.
assert.ok(
entry.models["opencode-omniroute/claude-sonnet-4-6"],
"live fetch model present (empty snapshot skipped)"
);
assert.equal(fetcher.callCount(), 1, "fetcher was called (awaited cold path)");
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +0,0 @@
---
feature: 9490
---
**Warm catalog startup from disk snapshot + parallel refresh** (opencode-plugin): The config-shim hook now reads the last disk snapshot *before* fetching, so the provider registers immediately with the last-known-good catalog (~1-2s vs ~30s on a warm gateway). All six fetchers run concurrently via `Promise.allSettled` instead of sequentially. A failed refresh keeps the snapshot (no overwrite). An in-flight guard prevents concurrent refreshes for the same cache key. The `features.diskCache: false` opt-out disables the warm read entirely.

View File

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

View File

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

View File

@@ -1 +0,0 @@
- test(combo): guard auto/best-free never leaks the combo name as a model (#7754)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1 +0,0 @@
- fix(build): include better-sqlite3 prebuilds in standalone bun bundle

View File

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

View File

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

View File

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

View File

@@ -1 +0,0 @@
- **fix(quota):** Deleting a quota pool now removes its scoped managed combos without racing in-flight pool mutations ([#8906](https://github.com/diegosouzapw/OmniRoute/pull/8906)) — thanks @xiaoyaner0201

View File

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

View File

@@ -1 +0,0 @@
- fix(github): add targetFormat to GPT-5.6 Sol/Terra/Luna models (#8951)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1 +0,0 @@
- fix(api): use configured prefix instead of raw node UUID for alias-backed model id in /v1/models (#9034)

View File

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

View File

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

View File

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

View File

@@ -1 +0,0 @@
- fix(api): auto/* routing aliases bypass API-key allowedConnections/disableNonPublicModels (#9057)

View File

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

View File

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

View File

@@ -1 +0,0 @@
- fix(providers): resolve combo names in audio transcriptions route so /v1/models stays honest (#9134)

View File

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

View File

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

View File

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

View File

@@ -1 +0,0 @@
- fix(model-discovery): ingest capabilities.effort_tiers for synced models (#9160)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1 +0,0 @@
- **fix(classify429):** add missing `have exhausted their quota` pattern so the synthetic 429 from auth.ts is recognized as quota exhaustion, preventing the combo loop from burning retries against the same provider instead of falling back to a healthy one ([#9269](https://github.com/diegosouzapw/OmniRoute/issues/9269))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1 +0,0 @@
- fix(providers): bump qwen-web SPA version header from 0.2.66 to 0.2.81 (#9304)

View File

@@ -1 +0,0 @@
- fix(sse): broaden OMNIROUTE_SSE_COMMENTS to accept 'false','0','no' and gate metadata comment emission (#9305)

View File

@@ -1 +0,0 @@
- fix(lmarena): encode SSE stream chunks as Uint8Array to prevent TextDecoder TypeError (#9306)

View File

@@ -1 +0,0 @@
- fix(backend): use accumulated responseBody for provider payload in dashboard log viewer to avoid stale data from truncated SSE events (#9315)

View File

@@ -1 +0,0 @@
- fix(qoder): surface qodercli stderr in error message instead of generic 502 (#9319)

View File

@@ -1 +0,0 @@
- **fix(providers):** Codex GPT-5.6 model metadata reports the 1M context window and 922K input limit ([#9431](https://github.com/diegosouzapw/OmniRoute/issues/9431)).

View File

@@ -1 +0,0 @@
- fix(providers): classify 400 out of extra usage as quota_exhausted for Anthropic OAuth

View File

@@ -1 +0,0 @@
- fix(api): make the 800-message chat history cap opt-in so long conversations reach compression instead of a terminal 413 (#9494)

View File

@@ -1 +0,0 @@
- **fix(standalone):** multipart uploads (`POST /v1/audio/transcriptions`) no longer hang — the WebDAV wrapper hands non-WebDAV requests to Next synchronously instead of losing the start of a streaming body ([#9580](https://github.com/diegosouzapw/OmniRoute/pull/9580))

View File

@@ -1 +0,0 @@
- **fix(docker):** standalone co-location now completes packages Next's file tracing materialized partially (package.json without its `main` payload) — unblocks the Docker Hub publish that failed on every v3.8.50 push with `Cannot find module '@atjsh/llmlingua-2/dist/index.js'` ([#9615](https://github.com/diegosouzapw/OmniRoute/pull/9615))

View File

@@ -1 +0,0 @@
- fix(resilience): failed connection test now sets a short cooldown so connections recover after transient outages (#9623)

View File

@@ -1 +0,0 @@
- fix(db): wire telemetry cleanup scheduler in Next.js startup path (#9624)

View File

@@ -1 +0,0 @@
- fix(db): align domain_cost_history cleanup cutoff with millisecond column (#9625)

View File

@@ -1 +0,0 @@
- fix(playground): surface provider model loading errors and offer retry (#9626)

View File

@@ -1 +0,0 @@
- fix(combo): distinguish pre-dispatch skips from genuine failures to prevent false 503 ALL_ACCOUNTS_INACTIVE (#9630)

View File

@@ -1 +0,0 @@
- fix(build): add build-next-isolated.mjs sibling imports to package.json files array

View File

@@ -1 +0,0 @@
- Fixed `GET`/`PUT`/`DELETE /api/memory/[id]` always failing with a 500 (`Primary backend "sqlite" not registered`) when the route was reached before any other memory endpoint in the same process.

View File

@@ -1 +0,0 @@
- Replaced hand-rolled body type checks with Zod validation in the plugins marketplace install route and the three Dario admin routes, restoring the `t06:route-validation` gate (Hard Rule #7).

View File

@@ -1 +0,0 @@
- **test(cli):** OpenCode plugin suite realigned to the bare-key static-catalog contract from #9178/#9175 (21 tests were red on every opencode-plugin CI run; 287/287 after) ([#9614](https://github.com/diegosouzapw/OmniRoute/pull/9614))

View File

@@ -1 +0,0 @@
- Removed the unused `RadarReferrals` type export left by the radar referral-links feature (#9697), returning the dead-code ratchet to its 227 baseline.

View File

@@ -96,7 +96,6 @@
"node-machine-id",
"omniglyph",
"open",
"opencode-ai",
"ora",
"parse5",
"pino",

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