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
59 changed files with 615 additions and 198 deletions

1
.eslintcache-probe Normal file

File diff suppressed because one or more lines are too long

View File

@@ -58,7 +58,7 @@ Repository map and Reference Documentation sections below.
| Services | `open-sse/services/` | Combo routing, rate limits, caching, etc |
| Database | `src/lib/db/` | SQLite domain modules (130 migrations) |
| Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic |
| MCP Server | `open-sse/mcp-server/` | 104 tools (42 base + memory/skill/agentSkill/pool/notion/obsidian/gamification/plugin modules), 3 transports (stdio / SSE / Streamable HTTP), 31 scopes |
| MCP Server | `open-sse/mcp-server/` | 105 tools (42 base + memory/skill/agentSkill/pool/notion/obsidian/gamification/plugin modules), 3 transports (stdio / SSE / Streamable HTTP), 31 scopes |
| A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol |
| Skills | `src/lib/skills/` | Extensible skill framework |
| Memory | `src/lib/memory/` | Persistent conversational memory |

0
MAX Normal file
View File

View File

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

View File

@@ -1,6 +1 @@
---
kind: feature
ref: "#9415"
---
New-API / One-API / Sub2API aggregator balance detection for compatible nodes. When a compatible provider node has the "Aggregator Gateway" toggle enabled, OmniRoute will query the aggregator's `/api/user/self` endpoint to detect the account balance. The dashboard shows the balance badge and quota-preflight routing skips exhausted accounts. The feature is gated by the `NEWAPI_AGGREGATOR_BALANCE` feature flag (default: off). A custom `quotaPerUnit` override is supported for aggregators that use a different rate than the default 500000 units/$1.
- **sse:** New-API / One-API / Sub2API aggregator balance detection for compatible nodes — with the "Aggregator Gateway" toggle on, OmniRoute queries the aggregator's `/api/user/self` to read the account balance, shows it as a dashboard badge and lets quota-preflight routing skip exhausted accounts. Gated by the `NEWAPI_AGGREGATOR_BALANCE` feature flag (default off), with a `quotaPerUnit` override for aggregators that do not use the default 500000 units/$1 rate ([#9415](https://github.com/diegosouzapw/OmniRoute/issues/9415))

View File

@@ -1,5 +1,4 @@
{
"_comment": "Congelamento da divida ESLint da migracao TypeScript 7 (release/v3.8.50, 2026-08-05; regenerado 2026-08-06 apos prune de entradas orfas). Gerado pelo modo nativo `eslint --suppress-all --suppressions-location config/quality/eslint-suppressions.json` (NODE_OPTIONS=--max-old-space-size=12288). Politica: violacao PRE-EXISTENTE fica suprimida aqui; violacao NOVA (fora deste arquivo) e vermelho imediato e deve ser corrigida, nunca adicionada. Entradas que deixarem de ocorrer sao podadas com `eslint --prune-suppressions` (o job 'No new ESLint warnings' falha com supressoes orfas). A baseline eslintWarnings em config/quality/quality-baseline.json e 0 — o valor real medido com estas supressoes aplicadas.",
"open-sse/executors/blackbox-web.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 1
@@ -2080,11 +2079,6 @@
"count": 5
}
},
"tests/unit/combo-cache-invalidation.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 5
}
},
"tests/unit/combo-context-length.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 2
@@ -3328,11 +3322,6 @@
"count": 2
}
},
"tests/unit/vertex-functioncall-id-3440.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 2
}
},
"tests/unit/vertex-media.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 3
@@ -3363,4 +3352,4 @@
"count": 5
}
}
}
}

View File

@@ -371,7 +371,7 @@
"tests/unit/model-sync-route.test.ts": 1016,
"tests/unit/models-catalog-route.test.ts": 1636,
"tests/unit/perplexity-web.test.ts": 1355,
"tests/unit/provider-models-route.test.ts": 1784,
"tests/unit/provider-models-route.test.ts": 1787,
"tests/unit/provider-validation-specialty.test.ts": 2985,
"tests/unit/providers-page-utils.test.ts": 1106,
"tests/unit/response-sanitizer.test.ts": 1063,
@@ -381,7 +381,7 @@
"tests/unit/stream-utils.test.ts": 2445,
"tests/unit/token-refresh-service.test.ts": 1378,
"tests/unit/translator-openai-responses-req.test.ts": 1194,
"tests/unit/translator-openai-to-gemini.test.ts": 1616,
"tests/unit/translator-openai-to-gemini.test.ts": 1619,
"tests/unit/translator-openai-to-kiro.test.ts": 1275,
"tests/unit/translator-resp-gemini-to-openai.test.ts": 1234,
"tests/unit/usage-service-hardening.test.ts": 1483,
@@ -607,5 +607,7 @@
"_rebaseline_2026_08_05_9323_agentrouter_waf_retry": "PR #9323 (fix(agentrouter): retry on 400 content-blocked + burst guard) own growth: open-sse/executors/base.ts 1578->1623 (check-file-size.mjs conta via split(\"\\n\").length; wc -l ve 1622). As +45 linhas sao o WAF_RETRY_CONFIG + o burst guard via gateOutboundRequest() para o WAF do agentrouter.org, com comentarios explicando o porque de cada mitigacao e cobertos por tests/unit/base-executor-waf-retry.test.ts e tests/unit/wafRateLimit.test.ts. Crescimento funcional legitimo, nao inchaco.",
"_rebaseline_2026_08_05_9529_own_growth": "PR #9529 own growth (base release/v3.8.50 medida EXATAMENTE nos frozen antigos, entao o modo base-relative #8522 nao cobre): open-sse/services/rateLimitManager.ts 1060->1105 (+45: helper applyLimiterSettings() que re-arma o heartbeat do reservoir apos updateSettings — fix do bug Bottleneck 2.19.5 que congelava a fila weighted; TDD em tests/unit/ratelimit-reservoir-refresh.test.ts); tests/integration/chat-pipeline.test.ts 1592->1598 (+6: User-Agent do codex derivado de getCodexClientVersion() em vez de literal pinado — teste-irmao alinhado ao contrato); tests/unit/provider-validation-specialty.test.ts 2980->2985 (+5: cobertura NOVA claude-web 429 -> valid:false, alinhamento #9406); open-sse/translator/response/openai-responses.ts 1174->1204 (+30: buildResponsesReasoningSummaryDelta MOVIDA do leaf pureHelpers.ts para o host — a funcao do #9500 muta stream state e violava o contrato do leaf puro; o LOC total do par host+leaf nao cresceu, o pureHelpers encolheu o mesmo tanto). Crescimento por fix de producao + cobertura adicional + realocacao arquitetural, nao inchaco.",
"_rebaseline_2026_08_06_v3850_inherited_drift_reconcile": "Reconciliacao 2026-08-06 do drift ACUMULADO da release/v3.8.50 apos o lote de merges de 08-05/06: 13 arquivos acima do frozen no tip puro 8180b49ce1 (medidos pelo proprio gate). O modo PR base-relative (#8522) deixa PRs inocentes passarem, e os rebaselines individuais dos PRs se perderam nas resolucoes sucessivas de conflito deste hot-file — o drift so aparece no modo absoluto (nightly/local). Crescimentos funcionais dos PRs mergeados: #9024 topology click-nav src/app/(dashboard)/dashboard/HomePageClient.tsx; #9324 OpenRouter enrich src/app/(dashboard)/dashboard/providers/page.tsx; #9329 quota card ordering src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx; #9193 context-window suffixes src/sse/handlers/chat.ts; #9332 nested Claude server tool ids open-sse/executors/base.ts; #9228 strip orphaned tool outputs open-sse/executors/codex.ts; #9236 nvidia tool-name normalize open-sse/executors/default.ts; #9314 nested tool_call validation open-sse/executors/kiro.ts; #9260 caller identity REST hops open-sse/mcp-server/server.ts; #8934 cache breakpoints tests tests/unit/chatcore-translation-paths.test.ts; #9193 suffix tests tests/unit/combo-routing-engine.test.ts; #9196 reasoning-on-tool-finish tests tests/unit/sse-auth.test.ts; #9163 GPT-5.6 Max reasoning tests tests/unit/translator-openai-to-kiro.test.ts. default.ts e kiro.ts entram no frozen (estavam sem entrada, acima do cap 1000). Atualizacao pos-medicao (a base avancou durante o ciclo do PR): src/sse/handlers/chat.ts 1857->1877 (#9184 affinity EOF evict) e open-sse/executors/default.ts 1027->1042 (#9005 Kimi K3 tool-name backfill).",
"_rebaseline_2026_08_06b_v3850_sweepreds_drift": "Segunda reconciliacao de 2026-08-06 (/sweep-reds sobre o tip puro 2ddbbc61a6): 3 arquivos voltaram a passar do frozen apos os merges do mesmo dia, com atribuicao 1:1 por commit. (1) src/app/(dashboard)/dashboard/providers/page.tsx 1928->1944 e (2) open-sse/executors/base.ts 1635->1640, ambos do #9515 (feat(radar): flag-gated signed free-model catalog overlay, commit e7f6b1d130) — o overlay do Radar entra por wiring nos chokepoints ja existentes (a resolucao/verificacao do catalogo assinado mora fora destes dois arquivos); +16 e +5 linhas liquidas nao sao extraiveis sem inventar um leaf por callsite. (3) open-sse/services/accountFallback.ts 1966->1972 do #8704 (commit c4527f97bd), +6 linhas de dados em CREDITS_EXHAUSTED_SIGNALS ('has been exhausted', fixes #8631). src/sse/handlers/chat.ts 1880>1877 tambem estava violando e NAO entra aqui de proposito: e drenado por encolhimento na PR #9598, sem rebaseline. Crescimento proprio DESTA PR: src/lib/db/migrationRunner.ts 1077->1084 (+7) — o guard retroativo em isSchemaAlreadyApplied para os arquivos renumerados 137/138, exigido pela propria mensagem de erro de colisao do runner (ambas as migracoes sao ALTER TABLE ADD COLUMN puro, nao idempotente). Dois `case` + dois `return hasColumn(...)` + 3 linhas de comentario dentro do switch existente; nao extraivel."
"_rebaseline_2026_08_06b_v3850_sweepreds_drift": "Segunda reconciliacao de 2026-08-06 (/sweep-reds sobre o tip puro 2ddbbc61a6): 3 arquivos voltaram a passar do frozen apos os merges do mesmo dia, com atribuicao 1:1 por commit. (1) src/app/(dashboard)/dashboard/providers/page.tsx 1928->1944 e (2) open-sse/executors/base.ts 1635->1640, ambos do #9515 (feat(radar): flag-gated signed free-model catalog overlay, commit e7f6b1d130) — o overlay do Radar entra por wiring nos chokepoints ja existentes (a resolucao/verificacao do catalogo assinado mora fora destes dois arquivos); +16 e +5 linhas liquidas nao sao extraiveis sem inventar um leaf por callsite. (3) open-sse/services/accountFallback.ts 1966->1972 do #8704 (commit c4527f97bd), +6 linhas de dados em CREDITS_EXHAUSTED_SIGNALS ('has been exhausted', fixes #8631). src/sse/handlers/chat.ts 1880>1877 tambem estava violando e NAO entra aqui de proposito: e drenado por encolhimento na PR #9598, sem rebaseline. Crescimento proprio DESTA PR: src/lib/db/migrationRunner.ts 1077->1084 (+7) — o guard retroativo em isSchemaAlreadyApplied para os arquivos renumerados 137/138, exigido pela propria mensagem de erro de colisao do runner (ambas as migracoes sao ALTER TABLE ADD COLUMN puro, nao idempotente). Dois `case` + dois `return hasColumn(...)` + 3 linhas de comentario dentro do switch existente; nao extraivel.",
"_rebaseline_2026_08_06c_v3850_sweepreds_pr2": "Segunda PR do /sweep-reds (fix/release-v3.8.50-basereds-0806b): tests/unit/provider-models-route.test.ts 1784->1787 (medido pelo gate, que conta split(\"\\n\").length) (+2 apos compressao de comentarios) — alinhamento de contrato forcado por dois merges do dia: #9106 tornou gemini-3.1-pro-high user-callable (a entry do alias entra na lista esperada do teste de discovery-retry, +1 linha de dado + 1 de comentario) e ff012ff420 adicionou onboardUser como bootstrap hop (exclusao no mock, ja comprimida a 1 linha). Nao ha o que encolher sem apagar o comentario que explica o porque.",
"_rebaseline_2026_08_07_v3850_sweepreds_pr2_toolnamemap": "tests/unit/translator-openai-to-gemini.test.ts 1616->1619 (+3). O frozen estava EXATAMENTE no tamanho da base, entao qualquer linha nova viola. #9568 (c9a3361e5a) fez buildChangedToolNameMap emitir entradas IDENTIDADE (o Gemini minusculiza nomes de tool nas respostas, entao o tradutor de resposta precisa da chave para mapear de volta), o que passou a incluir `_toolNameMap` no envelope Antigravity de qualquer request com tools. As 3 linhas sao: a chave nova na lista esperada de Object.keys, 1 comentario explicando POR QUE ela aparece (sem ele o proximo leitor tenta remove-la de novo) e 1 assert do CONTEUDO do map — presenca de chave sozinha nao provaria a entrada identidade, que e justamente o comportamento novo. Nao ha o que extrair: e alinhamento de contrato dentro de um teste existente."
}

View File

@@ -6,7 +6,7 @@ lastUpdated: 2026-06-28
# OmniRoute MCP Server Documentation
> Model Context Protocol server with 104 tools across routing, cache, compression, memory, skills, proxy, pool, and context source operations.
> Model Context Protocol server with 105 tools across routing, cache, compression, memory, skills, proxy, pool, and context source operations.
>
> Source of truth: `open-sse/mcp-server/server.ts` computes **104 unique tools** with `countUniqueMcpTools()`: 42 canonical definitions (including the six CCR lifecycle tools and the agent-skills trio), plus memory (3), skills (4), GitHub skills (3), pool (6), gamification (8), plugins (8), Notion (6), Obsidian (22), and two RTK-only compression tools.
@@ -369,7 +369,7 @@ MCP tool, prompt, and resource registries can compress descriptions at registrat
Description compression shrinks each tool's metadata; **tool-cardinality reduction** goes one step further by reducing _how many_ tools are announced at all. Advertising fewer tools in the `tools/list` manifest cuts the per-request token cost the client's model pays for the tool catalog ("layer 5" compression). The implementation is a pure, stateless filter in `open-sse/mcp-server/toolCardinality.ts` (`reduceToolManifest`), wired into the registration loop in `createMcpServer()` (`open-sse/mcp-server/server.ts`).
**Opt-in, off by default.** The filter only runs when at least one of two environment variables is set; with neither set, all 104 tools are announced unchanged.
**Opt-in, off by default.** The filter only runs when at least one of two environment variables is set; with neither set, all 105 tools are announced unchanged.
| Variable | Mode |
| :--------------- | :-------------------------------------------------------------------------------------- |

View File

@@ -645,7 +645,7 @@ for (const s of statuses) {
}
// Force re-check a specific proxy
invalidateProxyHealth("http://user:pass@1.2.3.4:8080");
invalidateProxyHealth("http://user:pass@203.0.113.7:8080");
```
Flaga `stale` jest `true`, gdy wpis cache przekroczył `HEALTH_CACHE_TTL_MS` i następne żądanie wywoła świeży check.
@@ -807,7 +807,7 @@ Gdy proxy systematycznie pada, oznacz je ręcznie, by rotator je pomijał:
```ts
import { failOneproxyProxy } from "omniroute/oneproxyRotator";
const removed = await failOneproxyProxy("1.2.3.4", 8080);
const removed = await failOneproxyProxy("203.0.113.7", 8080);
if (removed) {
console.log("Proxy marked as failed; rotator will skip it");
}

View File

@@ -645,7 +645,7 @@ for (const s of statuses) {
}
// Force re-check a specific proxy
invalidateProxyHealth("http://user:pass@1.2.3.4:8080");
invalidateProxyHealth("http://user:pass@203.0.113.7:8080");
```
The `stale` flag is `true` when the cache entry has exceeded `HEALTH_CACHE_TTL_MS` and the next request will trigger a fresh check.
@@ -807,7 +807,7 @@ When a proxy consistently fails, mark it manually so the rotator will skip it:
```ts
import { failOneproxyProxy } from "omniroute/oneproxyRotator";
const removed = await failOneproxyProxy("1.2.3.4", 8080);
const removed = await failOneproxyProxy("203.0.113.7", 8080);
if (removed) {
console.log("Proxy marked as failed; rotator will skip it");
}

View File

@@ -429,6 +429,7 @@ For deployments on small VPS instances (1 GB RAM or less):
- **Disable background services** — set `OMNIROUTE_DISABLE_BACKGROUND_SERVICES=1` to skip scheduler, MCP server, and periodic maintenance tasks. See `docs/reference/ENVIRONMENT.md`.
- **Use SQLite WAL mode** — enabled by default, reduces peak memory during concurrent reads.
- **Limit connection concurrency** — reduce `OMNIROUTE_MAX_POOL_SIZE` and `OMNIROUTE_DB_POOL_SIZE` in your environment.
- **Cap the V8 heap** — set `OMNIROUTE_MEMORY_MB` (e.g. `512`) so the runtime does not calibrate a ceiling larger than the VM. See `docs/reference/ENVIRONMENT.md`.
- **Limit concurrent heavy requests** — lower `OMNIROUTE_CHAT_MAX_HEAVY_IN_FLIGHT` (default `1`); excess requests get a retryable `503` with `Retry-After` instead of competing for memory.
- **Avoid `next build` on the VPS** — build locally and deploy the standalone output (`.next/standalone/`).
- **Monitor with `top` / `free -m`** — OmniRoute typically uses 200-400 MB RSS at idle on a 1 GB VM.

View File

@@ -22,9 +22,10 @@ describe("MCP Essential Tools", () => {
});
describe("Tool schema validation", () => {
it("should have exactly 11 essential tools (includes web_search + web_fetch + tool_search)", () => {
it("should have exactly 12 essential tools (includes web_search + web_fetch + tool_search)", () => {
// 11 -> 12: #8925 shipped omniroute_create_combo as a phase-1 tool.
const schemas = MCP_ESSENTIAL_TOOLS;
expect(schemas).toHaveLength(11);
expect(schemas).toHaveLength(12);
});
it("all tools should have omniroute_ prefix", () => {

View File

@@ -54,8 +54,21 @@ export function buildAccountSemaphoreKey({
return `${String(provider)}:${String(accountKey)}`;
}
/**
* Effective positive cap, or null when the semaphore is bypassed (unset/<=0).
*
* Narrowing companion of {@link isBypassed}: that one returns a plain boolean, so
* TypeScript cannot narrow `number | null` to `number` in its else-branch (a
* `x is null | undefined` predicate would be unsound — 0 bypasses too). Callers
* that need the VALUE after the guard go through here instead of casting.
*/
function resolveActiveCap(maxConcurrency?: number | null): number | null {
if (maxConcurrency == null || maxConcurrency <= 0) return null;
return maxConcurrency;
}
function isBypassed(maxConcurrency?: number | null): boolean {
return maxConcurrency == null || maxConcurrency <= 0;
return resolveActiveCap(maxConcurrency) === null;
}
function createNoopReleaseFn(): () => void {
@@ -192,7 +205,8 @@ export function acquire(
maxQueueSize = DEFAULT_MAX_QUEUE_SIZE,
}: AcquireAccountSemaphoreOptions = {}
): Promise<() => void> {
if (isBypassed(maxConcurrency)) {
const activeCap = resolveActiveCap(maxConcurrency);
if (activeCap === null) {
return Promise.resolve(createNoopReleaseFn());
}
@@ -200,7 +214,7 @@ export function acquire(
return Promise.reject(makeAbortError(signal));
}
const gate = ensureGate(semaphoreKey, maxConcurrency);
const gate = ensureGate(semaphoreKey, activeCap);
clearCleanupTimer(gate);
if (gate.running < gate.maxConcurrency && !isBlocked(gate)) {

View File

@@ -22,6 +22,43 @@ import { updateProviderConnection } from "@/lib/db/providers";
* Best-effort / non-fatal by design: a persistence failure must never block
* the in-flight request, which already has the discovered id in hand.
*/
/**
* Selection-side companion of the persistence write path (#8894): given a pool
* of Antigravity/AGY connections, prefer the ones that already carry a stored
* projectId — they can serve a request without the `loadCodeAssist` discovery
* round-trip. "Prefer", not "require": when NO connection has a stored project
* the pool is returned unchanged, so a fresh install never empties its
* candidate list.
*
* Sync on purpose (called inside the quota-strategy connection expansion, which
* builds candidate lists without awaiting per-connection work). Tolerates
* `providerSpecificData` arriving either parsed or as the raw DB JSON string.
*/
export function preferAntigravityConnectionsWithStoredProject<T extends Record<string, unknown>>(
connections: T[]
): T[] {
if (!Array.isArray(connections) || connections.length === 0) return connections;
const hasStoredProject = (connection: T): boolean => {
if (typeof connection.projectId === "string" && connection.projectId) return true;
let psd = connection.providerSpecificData;
if (typeof psd === "string") {
try {
psd = JSON.parse(psd);
} catch {
return false;
}
}
return Boolean(
psd &&
typeof psd === "object" &&
typeof (psd as Record<string, unknown>).projectId === "string" &&
(psd as Record<string, unknown>).projectId
);
};
const withStoredProject = connections.filter(hasStoredProject);
return withStoredProject.length > 0 ? withStoredProject : connections;
}
export async function persistDiscoveredAntigravityProjectId(
connectionId: string | undefined | null,
discoveredProjectId: string | undefined | null,

View File

@@ -137,7 +137,9 @@ function normalizeRuntimeStep(
: {}),
weight,
label,
prompt: step.prompt || null,
// `prompt` is a per-step pipeline input and only exists on a model step —
// #8894 widened the union with ComboProviderWildcardStep, which has no prompt.
prompt: (step.kind === "model" ? step.prompt : null) || null,
} satisfies ResolvedComboTarget;
}

View File

@@ -619,6 +619,7 @@ export async function tryRuntimeUnitDispatch(args: {
nesting: nestingContext,
baseOptions: buildBaseOptions(args),
runCombo: args.runCombo,
hiddenModelsByProvider: args.hiddenModelsByProvider,
});
recordRuntimeUnitStickySuccess({
strategy,

View File

@@ -10,7 +10,7 @@
* literal `auto/*` string panel member already behaves via the single-
* dispatch safety net in src/sse/handlers/chat.ts.
*/
import { normalizeComboStep } from "../../../src/lib/combos/steps.ts";
import { getComboModelString, normalizeComboStep } from "../../../src/lib/combos/steps.ts";
import { executeComboRefUnit } from "./runtimeUnits.ts";
import type {
ComboCollectionLike,
@@ -51,7 +51,11 @@ export function extractFusionPanelSpec(
panel.push(step.comboName);
return;
}
panel.push(step.model);
// #8894 widened ComboStep with ComboProviderWildcardStep, which carries a
// modelPattern instead of a model. getComboModelString() already resolves any
// step shape (and returns null for the ones with no concrete model id).
const modelStr = getComboModelString(step);
if (modelStr) panel.push(modelStr);
});
return { panel, comboRefUnits };
}

View File

@@ -45,7 +45,7 @@ import {
type QuotaFetchCacheConfig,
} from "./quotaScoring.ts";
import { rankByHeadroom, type HeadroomSaturation } from "./headroomRanking.ts";
import { preferAntigravityConnectionsWithStoredProject } from "../antigravityProjectPersistence.ts";
import { preferAntigravityConnectionsWithStoredProject } from "../antigravityProjectPersist.ts";
import { isQuotaExhaustedForRequest } from "../../../src/domain/quotaCache.ts";
const RESET_AWARE_CONNECTION_CACHE_TTL_MS = 30_000;

View File

@@ -9,7 +9,12 @@
import { isAccountSemaphoreFull } from "../accountSemaphore.ts";
import { resolveComboTargets } from "./comboStructure.ts";
import { lookupPositiveCap } from "./concurrencyCaps.ts";
import type { ComboCollectionLike, ComboLike, ResolvedComboUnit } from "./types.ts";
import type {
ComboCollectionLike,
ComboLike,
HiddenModelsByProvider,
ResolvedComboUnit,
} from "./types.ts";
type CapLookup = (connectionId: string) => Promise<number | null>;
@@ -45,7 +50,12 @@ async function isConnectionAtConcurrencyCap(
export async function isRuntimeUnitAtConcurrencyCap(
unit: ResolvedComboUnit,
allCombos: ComboCollectionLike,
lookupCap: CapLookup = lookupPositiveCap
lookupCap: CapLookup = lookupPositiveCap,
// Threaded from the caller so the hidden-model snapshot resolved once per
// request is reused. Without it resolveComboTargets falls back to its default
// getHiddenModelsByProvider(), i.e. a fresh full key_value read per nested
// combo-ref unit on EVERY request (#8878 threaded the other call sites).
hiddenModelsByProvider?: HiddenModelsByProvider
): Promise<boolean> {
if (unit.kind === "model") {
if (!unit.connectionId || !unit.provider) return false;
@@ -55,7 +65,7 @@ export async function isRuntimeUnitAtConcurrencyCap(
const childCombo = findComboByName(allCombos, unit.comboName);
if (!childCombo) return false;
const targets = resolveComboTargets(childCombo, allCombos, 1);
const targets = resolveComboTargets(childCombo, allCombos, 1, hiddenModelsByProvider);
const byConnection = new Map<string, { provider: string; connectionId: string }>();
for (const target of targets) {
if (!target.connectionId || !target.provider) continue;

View File

@@ -18,6 +18,7 @@ import type {
ComboNestingContext,
HandleComboChatOptions,
HandleSingleModel,
HiddenModelsByProvider,
IsModelAvailable,
ResolvedComboRefTarget,
ResolvedComboUnit,
@@ -186,6 +187,7 @@ export async function executeRuntimeUnitCombo(args: {
nesting: ComboNestingContext;
baseOptions: HandleComboChatOptions;
runCombo: RuntimeUnitRunner;
hiddenModelsByProvider?: HiddenModelsByProvider;
}): Promise<RuntimeUnitExecutionResult> {
const maxRetries = Number(args.config.maxRetries ?? 1);
const retryDelayMs = resolveDelayMs(args.config.retryDelayMs, 2000);
@@ -197,7 +199,14 @@ export async function executeRuntimeUnitCombo(args: {
let fallbackCount = 0;
for (const unit of orderedUnits) {
if (await isRuntimeUnitAtConcurrencyCap(unit, args.allCombos)) {
if (
await isRuntimeUnitAtConcurrencyCap(
unit,
args.allCombos,
undefined,
args.hiddenModelsByProvider
)
) {
args.log.info(
"COMBO",
`Skipping ${unit.kind} ${unitDisplayName(unit)} — concurrency cap reached`

View File

@@ -292,7 +292,8 @@ function rehydrateEntry(hash: string, principalId: string, now: number): CcrEntr
// Re-admit through the same budgets a fresh store would face. If the block no longer
// fits, it stays on disk and is served straight from the row instead of being cached.
if (enforcePrincipalBudget(entry.principalId, entry.bytes) && enforceGlobalBudget(entry.bytes)) {
const { principalId: owner, bytes } = entry;
if (enforcePrincipalBudget(owner, bytes) && enforceGlobalBudget(owner, bytes)) {
const key = buildStoreKey(hash, principalId === ANON ? undefined : principalId);
ccrStore.set(key, entry);
ccrTotalBytes += entry.bytes;

View File

@@ -110,7 +110,8 @@ export function getFirecrawlBaseUrl(connection?: Record<string, unknown>): strin
return envBase.replace(/\/+$/, "");
}
const providerData = toRecord(connection?.providerSpecificData);
const connBase = typeof connection?.baseUrl === "string" ? connection.baseUrl : providerData?.baseUrl;
const connBase =
typeof connection?.baseUrl === "string" ? connection.baseUrl : providerData?.baseUrl;
if (typeof connBase === "string" && connBase.trim() && !connBase.includes("api.firecrawl.dev")) {
return connBase.trim().replace(/\/+$/, "");
}
@@ -120,7 +121,11 @@ export function getFirecrawlBaseUrl(connection?: Record<string, unknown>): strin
export async function fetchFirecrawlQuota(
connectionId: string,
connection?: Record<string, unknown>
): Promise<QuotaInfo | null> {
// FirecrawlQuota, not the base QuotaInfo: every return here is a full credit
// breakdown (remainingCredits / planCredits / extraCreditsInferred / overPlan),
// and the narrower annotation made the custom-base literal below an excess-
// property error. FirecrawlQuota extends QuotaInfo, so callers are unaffected.
): Promise<FirecrawlQuota | null> {
const cached = quotaCache.get(connectionId);
if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
return cached.quota;

View File

@@ -9,15 +9,23 @@
import { createHmac, createHash, randomUUID } from "node:crypto";
import { resolvePublicCred } from "../utils/publicCreds.ts";
export const RAYCAST_CHAT_URL = "https://backend.raycast.com/api/v1/ai/chat_completions";
export const RAYCAST_MODELS_URL = "https://backend.raycast.com/api/v1/ai/models";
export const RAYCAST_DEFAULT_USER_AGENT =
"Raycast/1.104.20 (macOS Version 26.5.1 (Build 25F80))";
export const RAYCAST_DEFAULT_USER_AGENT = "Raycast/1.104.20 (macOS Version 26.5.1 (Build 25F80))";
export const RAYCAST_DEFAULT_EXPERIMENTAL = "chatBranching, mcpHTTPServer";
/** Community-extracted default; override via providerSpecificData.sigSecret or RAYCAST_SIG_SECRET. */
export const RAYCAST_DEFAULT_SIG_SECRET =
"6bc455473576ce2cd6f70426caff867aabbe3f7291c1a79681af5e8ce0ca1408";
/**
* Community-extracted default; override via providerSpecificData.sigSecret or
* RAYCAST_SIG_SECRET. Embedded through resolvePublicCred() per Hard Rule #11 —
* a public upstream credential must never be a string literal in the source
* (see docs/security/PUBLIC_CREDS.md).
*/
export const RAYCAST_DEFAULT_SIG_SECRET = resolvePublicCred(
"raycast_sig_secret",
"RAYCAST_SIG_SECRET"
);
export type RaycastCredentials = {
accessToken?: string;
@@ -69,9 +77,7 @@ export function raycastJwt(aid: string, secret: string): string {
const iat = Date.now() / 1000;
const header = base64UrlJson({ typ: "JWT", alg: "HS256" });
const payload = base64UrlJson({ aid, exp: iat + 60, iat });
const signature = createHmac("sha256", secret)
.update(`${header}.${payload}`)
.digest("base64url");
const signature = createHmac("sha256", secret).update(`${header}.${payload}`).digest("base64url");
return `${header}.${payload}.${signature}`;
}
@@ -111,7 +117,10 @@ export function resolveRaycastSecrets(credentials: RaycastCredentials): {
return { bearerToken, deviceId, aid, sigSecret };
}
export function buildRaycastHeaders(payload: string, credentials: RaycastCredentials): Record<string, string> {
export function buildRaycastHeaders(
payload: string,
credentials: RaycastCredentials
): Record<string, string> {
const { bearerToken, deviceId, aid, sigSecret } = resolveRaycastSecrets(credentials);
const psd = credentials.providerSpecificData || {};
const timestamp = Math.floor(Date.now() / 1000).toString();
@@ -137,7 +146,12 @@ export function contentToText(content: unknown): string {
return content
.map((part) => {
if (typeof part === "string") return part;
if (part && typeof part === "object" && "type" in part && (part as { type?: string }).type === "text") {
if (
part &&
typeof part === "object" &&
"type" in part &&
(part as { type?: string }).type === "text"
) {
return String((part as { text?: string }).text || "");
}
return "";

View File

@@ -5,7 +5,11 @@
* credits into the standard `{ plan, quotas }` response.
*/
import { fetchFirecrawlQuota, getFirecrawlBaseUrl, type FirecrawlQuota } from "../firecrawlQuotaFetcher.ts";
import {
fetchFirecrawlQuota,
getFirecrawlBaseUrl,
type FirecrawlQuota,
} from "../firecrawlQuotaFetcher.ts";
import { createQuotaFromUsage, parseResetTime } from "./quota.ts";
function createFirecrawlPlanQuota(q: FirecrawlQuota) {
@@ -29,7 +33,11 @@ function createFirecrawlPlanQuota(q: FirecrawlQuota) {
};
}
export async function getFirecrawlUsage(connectionId: string, apiKey?: string, connection?: Record<string, unknown>) {
export async function getFirecrawlUsage(
connectionId: string,
apiKey?: string,
connection?: Record<string, unknown>
) {
if (!connectionId) {
return { message: "Firecrawl: connection id unavailable." };
}
@@ -44,7 +52,12 @@ export async function getFirecrawlUsage(connectionId: string, apiKey?: string, c
}
try {
const live = await fetchFirecrawlQuota(connectionId, connection);
// The explicit `apiKey` argument was silently dropped when #91bb6aa619 moved
// this to fetchFirecrawlQuota(connectionId, connection): the fetcher reads the
// key off the connection record, so a caller that passes the key directly —
// without a connection carrying it — always got "API key not available".
const resolvedConnection = apiKey ? { ...(connection || {}), apiKey } : connection;
const live = await fetchFirecrawlQuota(connectionId, resolvedConnection);
if (!live) {
return { message: "Firecrawl API key not available or credit usage unavailable." };
}

View File

@@ -207,6 +207,15 @@ const EMBEDDED_DEFAULTS = {
// Firefly credits balance endpoint public x-api-key (`SunbreakWebUI1`) from
// GET firefly.adobe.io/v1/credits/balance browser traffic.
adobe_firefly_balance_api_key: [60, 24, 0, 11, 0, 10, 20, 31, 50, 72, 18, 32, 43, 93],
// Raycast Pro V2 request-signature secret (#8895). Community-extracted from the
// public Raycast macOS client — the SAME value ships to every install, so it is
// public by design, not a per-user credential. Overridable via RAYCAST_SIG_SECRET
// or providerSpecificData.sigSecret.
raycast_sig_secret: [
89, 15, 13, 93, 71, 90, 65, 67, 86, 24, 71, 67, 1, 9, 91, 0, 73, 64, 87, 88, 93, 90, 91, 68, 12,
20, 18, 3, 21, 70, 66, 3, 13, 11, 1, 72, 69, 87, 88, 95, 87, 88, 17, 94, 20, 67, 92, 27, 72, 68,
3, 10, 92, 6, 21, 21, 84, 95, 14, 15, 88, 70, 95, 77,
],
} as const;
export type EmbeddedDefaultKey = keyof typeof EMBEDDED_DEFAULTS;

View File

@@ -63,6 +63,7 @@ export const INTENTIONALLY_INTERNAL = new Set([
"optimizationSettings", // db-internal: imported by db/core.ts for SQLite PRAGMA application helpers that require the live adapter
"pluginMetrics", // DEAD? (production): write path não foi conectado ainda (documentado no cabeçalho do módulo); testado por tests/unit/plugins-metrics.test.ts
"prompts", // DEAD? (production): zero callers de produção encontrados; domínio domain/prompts.ts é independente; testado por tests/integration/proxy-pipeline.test.ts
"probeUtils", // db-internal: importado so por db/core.ts (retryProbeIfTransient no caminho da corruption-probe, #9541); testado por tests/unit/probe-9541-repro.test.ts
"providerNodeSelect", // db-internal: importado só por db/providers.ts (selectProviderNodeForConnection — lógica pura de seleção de provider node split do providers.ts, #4421)
"providerStats", // intentionally-internal: src/app/api/provider-stats/route.ts
"proxyLatency", // intentionally-internal: imported directly by src/lib/db/proxies.ts (anti-barrel, #6798)

View File

@@ -107,6 +107,11 @@ export const COLLECTORS = [
glob: "open-sse/services/__tests__/antigravity-quota-family.test.ts",
sources: ["vitest.mcp.config.ts"],
},
// #8890 landed this suite here without wiring a runner, so it had never run once.
{
glob: "open-sse/services/__tests__/fail-fast-concurrency-gate.test.ts",
sources: ["vitest.mcp.config.ts"],
},
{ glob: "tests/unit/autoCombo/**/*.test.ts", sources: ["vitest.mcp.config.ts"] },
{ glob: "src/lib/memory/__tests__/generic-backend.test.ts", sources: ["vitest.mcp.config.ts"] },
{ glob: "tests/unit/encryption.spec.ts", sources: ["vitest.mcp.config.ts"] },

View File

@@ -111,6 +111,14 @@ export const OAUTH_TEST_CONFIG = {
// Validate using token presence/expiry as a lightweight auth check.
checkExpiry: true,
},
raycast: {
// #8895 — Raycast Pro is an `import_token` provider: the token is imported
// from the local Raycast install, `refreshToken` is always null and the
// stored `expiresIn` defaults to 30 days. There is nothing to refresh, so
// the test is the expiry check on the imported token; without an entry here
// Test Connection persists testStatus="error" on a healthy account (#8408).
checkExpiry: true,
},
cline: CLINE_OAUTH_TEST_CONFIG,
// ClinePass reuses the same WorkOS OAuth flow and token lifecycle as Cline.
clinepass: CLINE_OAUTH_TEST_CONFIG,

View File

@@ -5475,13 +5475,13 @@
"newApiUserIdLabel": "ID de Usuário New-API",
"newApiUserIdPlaceholder": "ex.: 12345",
"newApiUserIdHint": "Valor do cabeçalho New-Api-User do AgentRouter, usado junto com a chave de API do console para consultar o saldo de cota.",
"newApiAggregatorToggleLabel": "Gateway Agregador",
"newApiAggregatorToggleHint": "Ativar detecção de saldo para nós agregadores New-API / One-API / Sub2API. O painel mostrará o badge de saldo e o roteamento de pré-voo de cota ignorará contas esgotadas.",
"newApiAggregatorConsoleApiKeyHint": "Token de Acesso do Sistema para o endpoint /api/user/self do agregador. Não é a chave de API de roteamento.",
"newApiAggregatorUserIdHint": "Valor do cabeçalho New-Api-User usado para consultar o saldo de cota do usuário do agregador.",
"newApiAggregatorQuotaPerUnitLabel": "Cota por Unidade",
"newApiAggregatorQuotaPerUnitHint": "Unidades de crédito New-API por $1 (padrão: 500000). Substitua se seu agregador usar uma taxa diferente.",
"featureFlagNewApiAggregatorBalanceDescription": "Ativar detecção de saldo para nós compatíveis de agregadores New-API / One-API / Sub2API",
"newApiAggregatorToggleLabel": "Gateway agregador",
"newApiAggregatorToggleHint": "Ativa a detecção de saldo para nós agregadores New-API / One-API / Sub2API. O painel passa a mostrar o selo de saldo e o roteamento com quota-preflight pula contas esgotadas.",
"newApiAggregatorConsoleApiKeyHint": "System Access Token para o endpoint /api/user/self do agregador. Não é a chave de API de roteamento.",
"newApiAggregatorUserIdHint": "Valor do cabeçalho New-Api-User usado para buscar o saldo de quota do usuário do agregador.",
"newApiAggregatorQuotaPerUnitLabel": "Quota por unidade",
"newApiAggregatorQuotaPerUnitHint": "Unidades de crédito New-API por US$ 1 (padrão: 500000). Substitua se o seu agregador usar outra taxa.",
"featureFlagNewApiAggregatorBalanceDescription": "Ativa a detecção de saldo para nós compatíveis New-API / One-API / Sub2API",
"cpaModeDisabledTitle": "Habilitar backend CLIProxyAPI para emulação OAuth mais profunda do Claude Code",
"cpaModeEnabledTitle": "Usando CLIProxyAPI para uma emulação mais profunda do Claude Code (uTLS, multi-conta, perfis de dispositivo)",
"customUserAgentHint": "Override opcional enviado upstream como cabeçalho User-Agent desta conexão.",
@@ -5597,7 +5597,7 @@
"tagGroupPlaceholder": "ex.: personal, work, team-a",
"testModel": "Test Model",
"testingModel": "Testing Model",
"modelTestQuotaTooltip": "Cota esgotada — reinicia amanhã ou precisa de recarga",
"modelTestQuotaTooltip": "Quota esgotada — reseta amanhã ou precisa de recarga",
"toggleOffShort": "OFF",
"toggleOnShort": "ON",
"tokenExpiredBadge": "Expirado",

View File

@@ -6020,7 +6020,15 @@
"ccAliasAddModelPlaceholder": "Id mô hình (ví dụ: gpt-4o)",
"ccAliasAddModelButton": "Thêm ghi đè",
"ccAliasLoadError": "Không tải được cài đặt bí danh khám phá: {error}",
"ccAliasSaveError": "Không lưu được cài đặt bí danh khám phá: {error}"
"ccAliasSaveError": "Không lưu được cài đặt bí danh khám phá: {error}",
"newApiAggregatorToggleLabel": "Cổng tổng hợp",
"newApiAggregatorToggleHint": "Bật phát hiện số dư cho các node tổng hợp New-API / One-API / Sub2API. Bảng điều khiển sẽ hiển thị huy hiệu số dư và định tuyến quota-preflight sẽ bỏ qua các tài khoản đã cạn.",
"newApiAggregatorConsoleApiKeyHint": "System Access Token cho endpoint /api/user/self của bộ tổng hợp. Không phải khóa API định tuyến.",
"newApiAggregatorUserIdHint": "Giá trị header New-Api-User dùng để lấy số dư quota của người dùng bộ tổng hợp.",
"newApiAggregatorQuotaPerUnitLabel": "Quota mỗi đơn vị",
"newApiAggregatorQuotaPerUnitHint": "Số đơn vị tín dụng New-API cho mỗi 1 USD (mặc định: 500000). Ghi đè nếu bộ tổng hợp của bạn dùng tỷ lệ khác.",
"featureFlagNewApiAggregatorBalanceDescription": "Bật phát hiện số dư cho các node tương thích New-API / One-API / Sub2API",
"modelTestQuotaTooltip": "Đã hết quota — sẽ đặt lại vào ngày mai hoặc cần nạp thêm"
},
"settings": {
"title": "Cài đặt",

View File

@@ -254,40 +254,3 @@ export function deleteSessionModelHistory(sessionId: string, comboName: string):
.run(sessionId, comboName);
return result.changes ?? 0;
}
/**
* Get usage counts for ALL models in a session's history.
* Returns a Map<{model}> -> {count} for least-used strategy.
*
* Queries the session_model_history table and aggregates by model_str.
* Can optionally filter by connectionId if provided.
*
* @param connectionId - Optional connection ID to filter by. If not provided, returns all connections.
* @returns Promise<Map<string, number>> of model strings to their usage count.
*/
export async function getSessionModelUsageCounts(
connectionId?: string
): Promise<Map<string, number>> {
const db = getDbInstance() as any;
let sql = `SELECT model_str, COUNT(*) as count
FROM session_model_history
WHERE 1=1`;
const params: unknown[] = [];
if (connectionId) {
sql += ` AND connection_id = ?`;
params.push(connectionId);
}
sql += ` GROUP BY model_str ORDER BY count ASC`;
const rows = db.prepare(sql).all(...params) as Array<{ model_str: string; count: number }>;
const usageMap = new Map<string, number>();
rows.forEach((row: { model_str: string; count: number }) => {
usageMap.set(row.model_str, row.count);
});
return usageMap;
}

View File

@@ -1,6 +1,10 @@
import { providerUsesAuthoritativeLiveCatalog } from "@omniroute/open-sse/config/providerRegistry";
import { PROVIDER_ID_TO_ALIAS } from "@omniroute/open-sse/config/providerModels.ts";
import { getSyncedAvailableModelsByConnection, type SyncedAvailableModel } from "../models";
import {
getSyncedAvailableModels,
getSyncedAvailableModelsByConnection,
type SyncedAvailableModel,
} from "../models";
import { getRawProviderConnections } from "../providers";
export type ActiveSyncedCatalog = {
@@ -102,11 +106,26 @@ export async function getActiveSyncedCatalog(providerId: string): Promise<Active
.map((connection) => connection.id);
const models = collectModelsForConnections(modelsByConnection, activeConnectionIds);
if (models.length > 0) {
return {
authoritative: providerUsesAuthoritativeLiveCatalog(providerId),
models,
};
}
return {
authoritative: models.length > 0 && providerUsesAuthoritativeLiveCatalog(providerId),
models,
};
// No ACTIVE CONNECTION carries a catalog for this provider — but a provider
// NODE can: nodes live in `provider_nodes`, never in `provider_connections`,
// so filtering by active connection ids drops their synced catalog entirely.
// Before #9294 this path read the provider-wide key_value set, and losing it
// took every node's runtime metadata with it (supportedThinkingEfforts, so
// `-high`/`-low` effort suffixes stopped resolving, plus contextWindow /
// maxInputTokens used by the combo context-window filter).
//
// Fall back to that provider-wide set, and deliberately keep it
// NON-authoritative: #9294's live-catalog gating is about what an active
// connection actually serves, so a node-backed catalog must inform metadata
// without ever being used to reject a model as unavailable.
return { authoritative: false, models: await getSyncedAvailableModels(storedProviderId) };
} catch {
return { authoritative: false, models: [] };
}

View File

@@ -232,13 +232,19 @@ export async function callVisionModel(
imageDataUri: string,
config: VisionModelConfig,
apiKey?: string,
routerConfig?: Partial<import("./visionBridgeRouter").VisionBridgeRouterConfig>
routerConfig?: Partial<import("./visionBridgeRouter").VisionBridgeRouterConfig>,
deps?: import("./visionBridgeRouter").VisionBridgeRouterDeps
): Promise<string> {
// Auto-select the best vision model
const modelToUse = await getBestVisionModel({
fixedModel: config.model,
...routerConfig,
});
// Auto-select the best vision model. `deps` is the router's existing
// injectable credential-check seam — without forwarding it, tests (and any
// embedder) cannot keep model selection away from the live connections DB.
const modelToUse = await getBestVisionModel(
{
fixedModel: config.model,
...routerConfig,
},
deps
);
// (#8430) When no vision-capable provider has usable credentials on this
// instance, surface a clear error instead of attempting a describe call that
// would fail with an opaque auth/serde error upstream.
@@ -248,7 +254,7 @@ export async function callVisionModel(
let lastError: Error | null = null;
// Try primary model + fallbacks
const modelsToTry = [modelToUse, ...(await getFallbackModels(modelToUse, routerConfig))];
const modelsToTry = [modelToUse, ...(await getFallbackModels(modelToUse, routerConfig, deps))];
const maxAttempts = Math.min(modelsToTry.length, routerConfig?.maxFallbackAttempts ?? 3);
for (let attempt = 0; attempt < maxAttempts; attempt++) {

View File

@@ -531,6 +531,10 @@ export const PROVIDERS = {
KIRO: "kiro",
AMAZON_Q: "amazon-q",
CURSOR: "cursor",
// #8895 — registered in src/lib/oauth/providers/index.ts but missing here, so
// every consumer reading PROVIDERS (onboarding wizard, test-connection routing)
// did not know Raycast Pro exists as an OAuth provider.
RAYCAST: "raycast",
KILOCODE: "kilocode",
CLINE: "cline",
CLINEPASS: "clinepass",

View File

@@ -64,6 +64,7 @@
"tests/unit/adobe-firefly.test.ts",
"tests/unit/anthropic-thinking-signature-recovery.test.ts",
"tests/unit/antigravity-429-quota-tdd.test.ts",
"tests/unit/antigravity-prefer-stored-project.test.ts",
"tests/unit/api-key-rotator-health.test.ts",
"tests/unit/appearance-widget-settings-schema.test.ts",
"tests/unit/auth-antigravity-account-retry-v2.test.ts",
@@ -206,6 +207,7 @@
"tests/unit/error-sensitive-redaction.test.ts",
"tests/unit/execute-chat-resource-pressure-breaker.test.ts",
"tests/unit/executor-antigravity.test.ts",
"tests/unit/executor-devin-cli-agentic-acp.test.ts",
"tests/unit/executor-web-cookie-sweep.test.ts",
"tests/unit/format-provider-error-cause.test.ts",
"tests/unit/forwarded-header-budget.test.ts",

View File

@@ -1488,6 +1488,29 @@
"stream": "devin://acp/stdio"
}
},
"devin-cli-agentic": {
"format": "claude",
"headers": {
"apiKey": {
"Accept": "text/event-stream",
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
},
"nonStream": {
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
},
"oauth": {
"Accept": "text/event-stream",
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
}
},
"url": {
"nonStream": "devin://acp/stdio",
"stream": "devin://acp/stdio"
}
},
"dgrid": {
"format": "openai",
"headers": {
@@ -4274,6 +4297,52 @@
"stream": "https://chat.qwen.ai/api/v2/chat/completions"
}
},
"raycast": {
"format": "openai",
"headers": {
"apiKey": {
"Accept": "text/event-stream",
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
},
"nonStream": {
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
},
"oauth": {
"Accept": "text/event-stream",
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
}
},
"url": {
"nonStream": "https://backend.raycast.com/api/v1/ai",
"stream": "https://backend.raycast.com/api/v1/ai"
}
},
"regolo": {
"format": "openai",
"headers": {
"apiKey": {
"Accept": "text/event-stream",
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
},
"nonStream": {
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
},
"oauth": {
"Accept": "text/event-stream",
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
}
},
"url": {
"nonStream": "https://api.regolo.ai",
"stream": "https://api.regolo.ai"
}
},
"reka": {
"format": "openai",
"headers": {
@@ -4829,6 +4898,29 @@
"stream": "https://hermes.ai.unturf.com/v1/chat/completions"
}
},
"unorouter": {
"format": "openai",
"headers": {
"apiKey": {
"Accept": "text/event-stream",
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
},
"nonStream": {
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
},
"oauth": {
"Accept": "text/event-stream",
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
}
},
"url": {
"nonStream": "https://api.unorouter.ai/v1/chat/completions",
"stream": "https://api.unorouter.ai/v1/chat/completions"
}
},
"upstage": {
"format": "openai",
"headers": {

View File

@@ -0,0 +1,52 @@
/**
* Regression guard for the #8894 import break: quotaStrategies.ts imported
* `preferAntigravityConnectionsWithStoredProject` from a module that never
* landed (`antigravityProjectPersistence.ts`), killing the whole combo module
* graph with ERR_MODULE_NOT_FOUND on the release tip. The helper now lives in
* the real persistence module (`antigravityProjectPersist.ts`); these tests pin
* its selection semantics.
*
* Run: node --import tsx/esm --test tests/unit/antigravity-prefer-stored-project.test.ts
*/
import { test } from "node:test";
import assert from "node:assert/strict";
import { preferAntigravityConnectionsWithStoredProject } from "../../open-sse/services/antigravityProjectPersist.ts";
const withProject = { id: "a", projectId: "proj-1" };
const withNestedProject = { id: "b", providerSpecificData: { projectId: "proj-2" } };
const withStringPsd = { id: "c", providerSpecificData: '{"projectId":"proj-3"}' };
const withoutProject = { id: "d", projectId: null, providerSpecificData: {} };
const withBrokenPsd = { id: "e", providerSpecificData: "{not json" };
test("prefers connections that already carry a stored projectId", () => {
const pool = [withoutProject, withProject, withNestedProject];
assert.deepEqual(
preferAntigravityConnectionsWithStoredProject(pool).map((c) => c.id),
["a", "b"]
);
});
test("reads projectId from a raw JSON-string providerSpecificData", () => {
const pool = [withoutProject, withStringPsd];
assert.deepEqual(
preferAntigravityConnectionsWithStoredProject(pool).map((c) => c.id),
["c"]
);
});
test("never empties the pool: no stored project anywhere → unchanged", () => {
const pool = [withoutProject, withBrokenPsd];
assert.deepEqual(preferAntigravityConnectionsWithStoredProject(pool), pool);
});
test("empty input passes through", () => {
assert.deepEqual(preferAntigravityConnectionsWithStoredProject([]), []);
});
test("the quota-strategy module graph resolves (the #8894 break shape)", async () => {
// Importing quotaStrategies transitively exercises the fixed import path; the
// pre-fix tip died here with ERR_MODULE_NOT_FOUND before any test could run.
const mod = await import("../../open-sse/services/combo/quotaStrategies.ts");
assert.ok(mod, "quotaStrategies must be importable");
});

View File

@@ -87,10 +87,15 @@ describe("detectModelFamily (pure)", () => {
});
it("advertises exactly one auto/<family> catalog id per family", () => {
assert.deepEqual(
[...AUTO_FAMILY_IDS].sort(),
["auto/gemini", "auto/gemma", "auto/glm", "auto/llama", "auto/mimo", "auto/minimax", "auto/zai"]
);
assert.deepEqual([...AUTO_FAMILY_IDS].sort(), [
"auto/gemini",
"auto/gemma",
"auto/glm",
"auto/llama",
"auto/mimo",
"auto/minimax",
"auto/zai",
]);
});
});
@@ -133,7 +138,10 @@ describe("auto/<family> materialization (#6453)", () => {
// "degrades gracefully" test below documents for opencode/minimax — a
// no-auth backend that genuinely serves a family model IS a legitimate
// member of the family pool, not just credentialed provider_connections rows.
assert.deepEqual(providerIds, ["auggie", "glm", "zai"]);
// `devin-cli-agentic` joined for the same documented reason as `auggie`:
// #8914 added the Devin ACP bridge whose catalog (registry/devin/catalog.ts)
// advertises the glm-5-2* line, so it genuinely serves the family.
assert.deepEqual(providerIds, ["auggie", "devin-cli-agentic", "glm", "zai"]);
// Every candidate must be a glm-family model (the Cartesian pool now surfaces
// each backend's full glm line-up, not only the glm-5.2 default), and the
// connected openai/gpt-4o-mini backend must be excluded — same family
@@ -201,7 +209,6 @@ describe("auto/<family> materialization (#6453)", () => {
);
});
it("rejects auto/<unknownfamily> with the same clean error as any unknown combo", async () => {
await assert.rejects(
() => builtinCatalog.createBuiltinAutoCombo("auto/unknownfam", "unknownfam"),

View File

@@ -119,6 +119,32 @@ describe("CCR engine survives losing its in-memory map (#9061)", () => {
);
});
it("re-admits the disk row into the fresh map (the enforceGlobalBudget arity bug)", async () => {
// The restart path re-admits a disk-served block through the same budgets a fresh
// store would face. #9061 called enforceGlobalBudget(entry.bytes) with ONE argument
// against a (owner, bytes) signature: `bytes` arrived undefined, `ccrTotalBytes +
// undefined` is NaN, and `NaN <= MAX` is false — so the re-admit never happened and
// the map stayed empty, re-reading from disk on every single retrieve. Typecheck
// caught the arity; this pins the observable behaviour.
const text = "z".repeat(2_000);
const stored = ccr.tryStoreBlock(text, "principal-readmit");
assert.equal(stored.stored, true);
await ccr.flushCcrDurableWrites();
const restarted = await import(`${ccrPath}?restart=9061-readmit`);
assert.equal(
restarted.getCcrStoreStats("principal-readmit").entries,
0,
"a fresh instance starts with an empty map"
);
assert.equal(restarted.retrieveBlock(stored.hash, "principal-readmit"), text);
assert.equal(
restarted.getCcrStoreStats("principal-readmit").entries,
1,
"the disk-served block must be re-admitted into the map, not re-read every time"
);
});
it("keeps the principal boundary across the restart", async () => {
const text = "y".repeat(2_000);
const stored = ccr.tryStoreBlock(text, "principal-a");

View File

@@ -250,7 +250,13 @@ test("chat completions route emits early keepalive while waiting for stream read
await seedHealthyConnection();
globalThis.fetch = async () => {
await new Promise((resolve) => setTimeout(resolve, 100));
// Must exceed resolveKeepaliveThreshold()'s DEFAULT_THRESHOLD_MS (2000ms) for
// openai/* — otherwise withEarlyStreamKeepalive takes the FAST path, forwards
// the handler response as-is and no keepalive frame is ever emitted. The old
// 100ms only worked while unrelated handler latency happened to push the
// total past the threshold, which made this assertion incidental rather than
// deterministic; it stopped holding once the handler got faster.
await new Promise((resolve) => setTimeout(resolve, 2_400));
return new Response(
[
`data: ${JSON.stringify({

View File

@@ -121,7 +121,7 @@ test("INTENTIONALLY_INTERNAL is exported from check-db-rules.mjs", () => {
assert.ok(INTENTIONALLY_INTERNAL.size > 0, "INTENTIONALLY_INTERNAL must not be empty");
});
test("INTENTIONALLY_INTERNAL contains the expected 37 audited modules", () => {
test("INTENTIONALLY_INTERNAL contains the expected 38 audited modules", () => {
const expected = [
"_rowTypes",
"accessTokens",
@@ -147,6 +147,7 @@ test("INTENTIONALLY_INTERNAL contains the expected 37 audited modules", () => {
"optimizationSettings",
"pluginMetrics",
"prompts",
"probeUtils",
"providerNodeSelect",
"providerStats",
"proxyLatency",

View File

@@ -56,7 +56,11 @@ function capabilityEntry(limitContext: number | null) {
};
}
function capabilityEntryWithLimits(limitInput: number | null, limitContext: number | null, limitOutput = 4096) {
function capabilityEntryWithLimits(
limitInput: number | null,
limitContext: number | null,
limitOutput = 4096
) {
return {
...capabilityEntry(limitContext),
limit_input: limitInput,
@@ -325,10 +329,12 @@ test("small input-only maxInputTokens keeps a target whose input fits even thoug
);
});
test("input-only maxInputTokens still rejects when the input itself exceeds the cap", () => {
// The fix must not let a genuinely-too-small input cap pass. `too-small` has
// maxInputTokens = 1, which cannot even hold the ~11-token input, so it must
// still be dropped while the compatible target survives.
test("input-only maxInputTokens is demoted when the input itself exceeds the cap", () => {
// #8944 made context metadata ADVISORY: a catalog-too-small target is no longer
// removed (a stale catalog entry must never delete the only target that could
// accept the request at runtime), it is ordered AFTER the known-fitting ones.
// `too-small` has maxInputTokens = 1, which cannot hold the ~11-token input, so
// it must lose the ordering to `huge` while remaining available as a fallback.
saveModelsDevCapabilities({
"unit-7039-too-small": {
"too-small": capabilityEntryWithLimits(1, 1_000_000, 500),
@@ -344,14 +350,14 @@ test("input-only maxInputTokens still rejects when the input itself exceeds the
assert.deepEqual(
out.map((entry) => entry.modelStr),
["unit-7039-too-small/huge"]
["unit-7039-too-small/huge", "unit-7039-too-small/too-small"]
);
});
test("maxInputTokens defaulting to contextWindow still rejects when input + output exceeds the total window (#7039 follow-up)", () => {
// Shared-window model where maxInputTokens equals the total window size.
// The input alone fits the input cap, but input + output overflows the
// window, so the target must be rejected instead of passing on the input cap.
test("maxInputTokens defaulting to contextWindow is demoted when input + output exceeds the total window (#7039 follow-up)", () => {
// Shared-window model where maxInputTokens equals the total window size. The
// input alone fits the input cap but input + output overflows the window, so the
// target must not be PREFERRED — since #8944 it is demoted rather than dropped.
saveModelsDevCapabilities({
"unit-7039-window": {
"shared-window": capabilityEntryWithLimits(400_000, 400_000, 200_000),
@@ -367,7 +373,7 @@ test("maxInputTokens defaulting to contextWindow still rejects when input + outp
assert.deepEqual(
out.map((entry) => entry.modelStr),
["unit-7039-window/huge"]
["unit-7039-window/huge", "unit-7039-window/shared-window"]
);
});
@@ -391,16 +397,18 @@ test("model_context_override lets a small-catalog target survive a large-context
largeContextBody(),
noopLog
);
assert.deepEqual(
out.map((entry) => entry.modelStr).sort(),
["unit-override/big", "unit-override/capped"]
);
assert.deepEqual(out.map((entry) => entry.modelStr).sort(), [
"unit-override/big",
"unit-override/capped",
]);
} finally {
removeModelContextOverride("unit-override", "capped");
}
});
test("without an override the small-catalog target is still dropped for the large request", () => {
// #8944: "dropped" became "demoted" — the small-catalog target survives as a
// runtime fallback but must never outrank the one whose known limit fits.
test("without an override the small-catalog target is ordered last for the large request", () => {
saveModelsDevCapabilities({
"unit-override": {
big: capabilityEntry(1_000_000),
@@ -417,6 +425,6 @@ test("without an override the small-catalog target is still dropped for the larg
assert.deepEqual(
out.map((entry) => entry.modelStr),
["unit-override/big"]
["unit-override/big", "unit-override/capped"]
);
});

View File

@@ -8,8 +8,15 @@ import { sessionDedupEngine } from "../../../open-sse/services/compression/engin
const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "../../..");
const FIXTURE = join(REPO_ROOT, "tests/fixtures/compression/session-dedup-memory-7849.ts");
// #7849 originally bounded session-dedup with a shared "suffix work budget" that
// emitted SUFFIX_WORK_BUDGET_WARNING when exhausted. Commit 7f36b192f0 REPLACED
// that mechanism with the MAX_SUFFIX_STARTS / MAX_TOTAL_BLOCK_BYTES guards and
// removed both the budget and its warning. The invariant #7849 exists for — a
// line-rich long context must not blow the heap, and the engine must fail open —
// is unchanged and still guarded below; only the pins on the removed mechanism
// were rewritten. The budget size is kept as the input-shaping constant that
// produces the pathological pair.
const SUFFIX_WORK_BUDGET = 32 * 1024 * 1024;
const SUFFIX_WORK_BUDGET_WARNING = "session-dedup: skipped (suffix work budget exceeded)";
function makeFixedWidthText(lineCount: number, lineChars: number, tag: string): string {
return Array.from({ length: lineCount }, (_, index) => {
@@ -38,7 +45,7 @@ function makeSharedBudgetBody(): Record<string, unknown> {
};
}
test("#7849: shares the two-pass suffix-work budget across all messages", () => {
test("#7849: the two-message pathological pair stays bounded", () => {
const body = makeSharedBudgetBody();
const messages = body.messages as Array<{ content: string }>;
const perMessageWork = messages.map(({ content }) => projectedSuffixWork(content, 2));
@@ -64,20 +71,29 @@ test("#7849: shares the two-pass suffix-work budget across all messages", () =>
assert.equal(individualResult.stats, null, "each message must be accepted individually");
}
// The pathological pair must be processed BOUNDED — quickly and without
// corrupting the body. Pre-#7849 this shape retained one full-length suffix
// string per line and OOM-killed the heap.
const started = Date.now();
const result = sessionDedupEngine.apply(body);
assert.deepEqual(result.stats?.validationWarnings, [SUFFIX_WORK_BUDGET_WARNING]);
assert.ok(
Date.now() - started < 4000,
"the pathological pair must stay fast; quadratic work would take seconds"
);
assert.ok(Array.isArray((result.body as { messages?: unknown[] }).messages));
});
test("#7849: exhausted suffix-work budget fails open with exact zero-savings stats", () => {
test("#7849: the pathological pair fails open, returning the input body untouched", () => {
const body = makeSharedBudgetBody();
const result = sessionDedupEngine.apply(body);
assert.strictEqual(result.body, body, "budget exhaustion must return the input body by identity");
// Fail-open is the surviving contract: nothing deduplicable in this shape, so
// the ORIGINAL body comes back by identity and no compression is claimed. The
// explanatory zero-savings stats belonged to the removed budget path — the
// current guards skip before producing any, so stats is null.
assert.strictEqual(result.body, body, "failing open must return the input body by identity");
assert.equal(result.compressed, false);
assert.ok(result.stats, "budget exhaustion must return explanatory stats");
assert.equal(result.stats.originalTokens, result.stats.compressedTokens);
assert.equal(result.stats.savingsPercent, 0);
assert.deepEqual(result.stats.validationWarnings, [SUFFIX_WORK_BUDGET_WARNING]);
assert.equal(result.stats, null);
});
test("#7849: near-boundary under-budget request still deduplicates", () => {
@@ -130,9 +146,13 @@ test(
warnings: string[];
};
assert.deepEqual(output.enginesRun, ["session-dedup", "lite", "rtk", "headroom", "caveman"]);
// The OOM guard is `child.status === 0` plus the full engine chain above:
// pre-fix this fixture killed the 512 MiB heap before the pipeline finished.
// session-dedup must still REPORT its skip; the exact reason string moved
// with the mechanism (7f36b192f0), so only the prefix is pinned.
assert.ok(
output.warnings.includes("session-dedup: skipped (suffix work budget exceeded)"),
`expected an explicit session-dedup work-budget warning, got ${JSON.stringify(output.warnings)}`
output.warnings.some((warning) => warning.startsWith("session-dedup: skipped")),
`expected a session-dedup skip warning, got ${JSON.stringify(output.warnings)}`
);
}
);

View File

@@ -30,13 +30,13 @@ const {
isControlPlaneProxyDirectFallbackEnabled,
} = await import("../../src/shared/utils/featureFlags.ts");
const EXPECTED_FEATURE_FLAG_COUNT = 45;
const EXPECTED_FEATURE_FLAG_COUNT = 46;
// ──────────────────────────────────────────────────────
// Test group 1 — Flag definitions registry
// ──────────────────────────────────────────────────────
describe("featureFlagDefinitions", () => {
it("has exactly 45 flag definitions", () => {
it("has exactly 46 flag definitions", () => {
assert.strictEqual(FEATURE_FLAG_DEFINITIONS.length, EXPECTED_FEATURE_FLAG_COUNT);
});
@@ -332,7 +332,7 @@ describe("resolveFeatureFlag", () => {
});
describe("resolveAllFeatureFlags", () => {
it("returns all 45 flags", () => {
it("returns all 46 flags", () => {
const all = resolveAllFeatureFlags();
assert.strictEqual(all.length, EXPECTED_FEATURE_FLAG_COUNT);
});

View File

@@ -21,7 +21,19 @@
*/
import test from "node:test";
import assert from "node:assert/strict";
import { callVisionModel, type VisionModelConfig } from "@/lib/guardrails/visionBridgeHelpers";
import {
callVisionModel as callVisionModelRaw,
type VisionModelConfig,
} from "@/lib/guardrails/visionBridgeHelpers";
// Inject the router's credential-check seam as INDETERMINATE (null): the fixed
// model is used as-is and selection never touches the live connections DB — on a
// clean box the suite otherwise dies with "No vision-capable provider connected",
// and on a dev box auto-selection may swap the model under the assertions.
const callVisionModel = (img: string, config: VisionModelConfig) =>
callVisionModelRaw(img, config, undefined, undefined, {
hasUsableCredentials: async () => null,
});
const originalFetch = globalThis.fetch;

View File

@@ -510,8 +510,14 @@ test("v1 image edit POST executes Codex through the configured connection proxy"
host: "127.0.0.1",
port: 1,
});
// #9100: the reachability probe is NON-BLOCKING — dispatch is optimistic and the
// probe aborts the request only while it is still in flight (t14 pattern). The
// mock must stay pending: an instantly-throwing fetch would settle the race
// first and surface as a generic 502 upstream error instead of the proxy 503.
// Never resolved on purpose so the aborted continuation cannot proceed.
globalThis.fetch = async () => {
throw new Error("Direct fetch must not run when the configured proxy is unreachable");
await new Promise(() => {});
throw new Error("unreachable");
};
const response = await imageEditRoute.POST(
@@ -537,8 +543,11 @@ test("v1 image generation POST resolves proxy and executes with proxy context wh
port: 1, // intentionally unreachable — proves proxy path was taken
});
// #9100 non-blocking probe: keep the request in flight so the fast-fail can
// abort it with the proxy-specific 503 (see the edit-route case above).
globalThis.fetch = async () => {
throw new Error("fetch should not be called — proxy fast-fail should trigger first");
await new Promise(() => {});
throw new Error("unreachable");
};
const response = await imageRoute.POST(

View File

@@ -46,6 +46,8 @@ test("public login bootstrap route exposes the metadata the login page consumes"
assert.equal(response.status, 200);
assert.deepEqual(body, {
// #9491 added `authenticated` so /login can redirect an active session.
authenticated: false,
requireLogin: true,
hasPassword: false,
setupComplete: true,
@@ -68,6 +70,8 @@ test("public login bootstrap route reports env-provided bootstrap password metad
assert.equal(response.status, 200);
assert.deepEqual(body, {
// #9491 added `authenticated` so /login can redirect an active session.
authenticated: false,
requireLogin: true,
hasPassword: true,
setupComplete: true,
@@ -89,6 +93,8 @@ test("public login bootstrap route reports stored password metadata and disabled
assert.equal(response.status, 200);
assert.deepEqual(body, {
// #9491 added `authenticated` so /login can redirect an active session.
authenticated: false,
requireLogin: false,
hasPassword: true,
setupComplete: true,

View File

@@ -6,7 +6,7 @@ import assert from "node:assert/strict";
// (omniroute_agent_skills_list/get/coverage) are intentionally defined in BOTH
// MCP_TOOLS (open-sse/mcp-server/schemas/tools.ts) and agentSkillTools
// (open-sse/mcp-server/tools/agentSkillTools.ts), so the additive sum reported 121
// while only 107 distinct tool names actually exist. countUniqueMcpTools
// while only 108 distinct tool names actually exist. countUniqueMcpTools
// (open-sse/mcp-server/toolCount.ts) fixes this by unioning tool names from every
// registered collection into a Set, so each user-visible tool is counted once.
@@ -60,7 +60,7 @@ test("#6854: countUniqueMcpTools de-duplicates tools registered in multiple coll
};
const total = countUniqueMcpTools(collections);
assert.equal(total, 107, "the published MCP inventory must match the registered tool set");
assert.equal(total, 108, "the published MCP inventory must match the registered tool set");
// Independently compute the "true" unique count by unioning every collection's
// tool names into a Set — this must equal countUniqueMcpTools's own result AND

View File

@@ -40,6 +40,7 @@ const {
OAUTH_TIMEOUT,
PROVIDERS: OAUTH_PROVIDER_IDS,
QODER_CONFIG,
RAYCAST_CONFIG,
TRAE_CONFIG,
WINDSURF_CONFIG,
XAI_OAUTH_CONFIG,
@@ -63,6 +64,7 @@ const EXPECTED_PROVIDER_KEYS = [
"amazon-q",
"cursor",
"trae",
"raycast",
"kilocode",
"cline",
"clinepass",
@@ -100,6 +102,7 @@ const EXPECTED_CONFIG_BY_PROVIDER = {
clinepass: CLINE_CONFIG, // reuses the Cline WorkOS flow (clinepass: cline in providers/index.ts)
windsurf: WINDSURF_CONFIG,
"devin-cli": WINDSURF_CONFIG,
raycast: RAYCAST_CONFIG,
trae: TRAE_CONFIG,
"grok-cli": GROK_BUILD_OAUTH_CONFIG,
"xai-oauth": XAI_OAUTH_CONFIG,

View File

@@ -101,6 +101,13 @@ test("Claude provider limits fail closed when an account proxy is unreachable",
await withMockedFetch(
(async (url) => {
directFetchUrls.push(String(url));
// #9100: the reachability probe is NON-BLOCKING — dispatch is optimistic and
// the probe aborts the request only while it is still in flight (same shape
// as t14-proxy-fast-fail). The mock must therefore stay pending: an instant
// response would win the race and the fast-fail would never be observable.
// Never resolved on purpose — the aborted continuation must NOT proceed to a
// real (unmocked) fetch after this block restores globalThis.fetch.
await new Promise(() => {});
return claudeUsageResponse();
}) as typeof fetch,
async () => {
@@ -108,10 +115,20 @@ test("Claude provider limits fail closed when an account proxy is unreachable",
() => providerLimits.fetchAndPersistProviderLimits(connectionId, "manual"),
/Proxy unreachable|fetch failed|ECONNREFUSED|UND_ERR_CONNECT_TIMEOUT/i
);
// The fail-closed proof is twofold: (1) the rejection above settled at all —
// a direct retry would await the hung mock and never reject; (2) nothing
// egresses AFTER the fast-fail. The in-flight count itself may be >1: the
// Claude flow dispatches bootstrap + oauth/usage concurrently, and both are
// optimistic pre-abort attempts, not retries.
const urlsAtRejection = directFetchUrls.length;
await new Promise((resolve) => setTimeout(resolve, 50));
assert.equal(
directFetchUrls.length,
urlsAtRejection,
"account-proxied Claude usage must not egress anything after the fast-fail"
);
}
);
assert.deepEqual(directFetchUrls, [], "account-proxied Claude usage must not retry direct");
});
test("non-Claude OAuth provider limits fail closed when an account proxy is unreachable", async () => {
@@ -136,6 +153,9 @@ test("non-Claude OAuth provider limits fail closed when an account proxy is unre
await withMockedFetch(
(async (url) => {
directFetchUrls.push(String(url));
// #9100: non-blocking probe — keep the request in flight so the fast-fail
// can abort it (see the Claude case above for the full rationale).
await new Promise(() => {});
return new Response(
JSON.stringify({
copilot_plan: "free",
@@ -153,7 +173,10 @@ test("non-Claude OAuth provider limits fail closed when an account proxy is unre
}
);
assert.deepEqual(directFetchUrls, [], "account-proxied OAuth usage must not retry direct");
assert.ok(
directFetchUrls.length <= 1,
"at most the single optimistic in-flight attempt — account-proxied OAuth usage must never retry direct after the fast-fail"
);
});
test("Claude provider limits preserve direct retry for non-account proxy failures", async () => {

View File

@@ -934,7 +934,8 @@ test("provider models route retries Antigravity discovery endpoints before retur
// After PR #2219, the discovery flow calls loadCodeAssist first as a project
// bootstrap; treat all bootstrap calls as non-fatal failures so the test
// exercises the discovery retry path.
if (urlString.includes("/v1internal:loadCodeAssist")) {
// onboardUser is a bootstrap hop too (ff012ff420) — else it eats the single 503 below.
if (urlString.includes(":loadCodeAssist") || urlString.includes(":onboardUser")) {
return new Response("nope", { status: 503 });
}
seenUrls.push(urlString);
@@ -983,6 +984,8 @@ test("provider models route retries Antigravity discovery endpoints before retur
"https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels",
]);
assert.deepEqual(body.models, [
// #9106: both alias ids are user-callable now, so the upstream echo survives the filter.
{ id: "gemini-3.1-pro-high", name: "Gemini 3.1 Pro (High)" },
{ id: "gemini-pro-agent", name: "Gemini 3.1 Pro (High)" },
{ id: "gemini-3.6-flash-high", name: "Gemini 3.6 Flash (High)" },
{ id: "gemini-3.6-flash-medium", name: "Gemini 3.6 Flash (Medium)" },

View File

@@ -1,7 +1,7 @@
// Characterization of the providers.ts catalog split (god-file decomposition): the host became a
// barrel that re-exports 10 data catalogs now living under constants/providers/*, and APIKEY is
// merged from 6 semantic family files (apikey/<family>.ts). Locks: the public surface (every catalog
// + helpers still exported), the spread-merge integrity (197 APIKEY entries, no loss/dup), and that
// + helpers still exported), the spread-merge integrity (198 APIKEY entries, no loss/dup), and that
// load-time Zod validation still runs. Pure-data move → behavior must be identical.
// Count was 171 before obsolete provider removals (PR #6675: glhf/kluster/cablyai/inclusionai etc.,
// 171->167) plus #6126 (ClinePass dual-auth): the API-key-only APIKEY_PROVIDERS_GATEWAYS entry was
@@ -17,7 +17,7 @@
// sarvam+plamo in regional) to 193, then #8170 (inception/typhoon — inception in frontier-labs,
// typhoon in regional) to 195, then Firecrawl dual search+fetch under SEARCH_PROVIDERS.firecrawl
// (removed specialty-media duplicate) to 194, #8861 (Xiaomi MiMo Token Plan, regional) to 195, and
// the Cheaper Inference gateway (OSS-sponsor reseller, gateways family) to 197 (UnoRouter, #9009).
// the Cheaper Inference gateway (OSS-sponsor reseller, gateways family) to 198 (UnoRouter #9009, Raycast Pro #8895).
import { test } from "node:test";
import assert from "node:assert/strict";
@@ -46,12 +46,12 @@ test("barrel still exports every catalog + key helpers", () => {
}
});
test("APIKEY_PROVIDERS merges the 6 family files into 197 entries (no loss / no dup)", async () => {
test("APIKEY_PROVIDERS merges the 6 family files into 198 entries (no loss / no dup)", async () => {
const keys = Object.keys((P as Record<string, object>).APIKEY_PROVIDERS);
assert.equal(keys.length, 197);
assert.equal(new Set(keys).size, 197, "duplicate keys after spread-merge");
assert.equal(keys.length, 198);
assert.equal(new Set(keys).size, 198, "duplicate keys after spread-merge");
// the merged object's entry-count equals the sum of the 6 semantic family files; families are a
// strict partition (every provider in exactly one), so the sum must be exactly 197.
// strict partition (every provider in exactly one), so the sum must be exactly 198.
const families: [string, string][] = [
["gateways", "APIKEY_PROVIDERS_GATEWAYS"],
["frontier-labs", "APIKEY_PROVIDERS_FRONTIER"],
@@ -71,7 +71,7 @@ test("APIKEY_PROVIDERS merges the 6 family files into 197 entries (no loss / no
seen.add(k);
}
}
assert.equal(famTotal, 197, "families must partition all 197 providers");
assert.equal(famTotal, 198, "families must partition all 198 providers");
});
test("AI_PROVIDERS Proxy aggregates all sections; lookups resolve", () => {

View File

@@ -85,15 +85,20 @@ test("reservoir keeps refreshing after updateSettings() touches an already-heart
// number of event-loop ticks: Bottleneck's own updateSettings() goes through
// at least one real setTimeout(0) (yieldLoop) before storeOptions reflects the
// new value.
// #9604 replaced Bottleneck's fixed-window reservoir with the rolling lease gate
// (open-sse/services/rollingRpmGate.ts), so `reservoir` is null now and pinning it
// would assert a mechanism that no longer exists. What must still hold — and what
// the Bottleneck heartbeat bug actually broke — is that the limiter SURVIVES the
// header-learned updateSettings() and keeps admitting work (steps 3 and 4 below).
const pollDeadline = Date.now() + 2000;
let state = await rateLimitManager.__getLimiterStateForTests(PROVIDER, CONNECTION_ID, null);
while (state?.reservoir !== 2 && Date.now() < pollDeadline) {
while (!state && Date.now() < pollDeadline) {
await wait(10);
state = await rateLimitManager.__getLimiterStateForTests(PROVIDER, CONNECTION_ID, null);
}
assert.equal(state?.reservoir, 2, "reservoir must land at 2 before the slots below are consumed");
assert.ok(state, "the limiter must still exist after the header-learned update");
// 3. Consume both reservoir slots.
// 3. Consume the learned capacity.
assert.equal(
await rateLimitManager.withRateLimit(PROVIDER, CONNECTION_ID, null, async () => "slot-1"),
"slot-1"
@@ -103,10 +108,9 @@ test("reservoir keeps refreshing after updateSettings() touches an already-heart
"slot-2"
);
// 4. Reservoir is now 0. A healthy Bottleneck heartbeat refills it ~1s later
// from the reservoirRefreshInterval/reservoirRefreshAmount configured above.
// Race a 3rd request against a 5s timer: if the heartbeat died (unfixed bug),
// the request stays QUEUED forever and the timer wins instead.
// 4. Capacity is spent. Race a 3rd request against a 5s timer: if the limiter
// stopped pacing after updateSettings() (the original bug) the request stays
// queued forever and the timer wins instead.
const RACE_TIMEOUT_MS = 5000;
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
const timeout = new Promise<"timed-out">((resolve) => {
@@ -125,8 +129,8 @@ test("reservoir keeps refreshing after updateSettings() touches an already-heart
assert.equal(
result,
"slot-3",
'reservoir must refresh ~1s after being exhausted; "timed-out" means the Bottleneck ' +
"heartbeat died after updateSettings() and the reservoir never refilled " +
"(node_modules/bottleneck/lib/LocalDatastore.js _startHeartbeat clearInterval-without-null bug)"
'capacity must recover after being exhausted; "timed-out" means the limiter stopped ' +
"admitting work after the header-learned updateSettings() the failure shape of the " +
"original Bottleneck heartbeat bug (LocalDatastore _startHeartbeat clearInterval-without-null)"
);
});

View File

@@ -260,7 +260,10 @@ test("response.completed normalizes usage when lifecycle echoes are stripped", a
const completed = JSON.parse(completedLine.slice(5).trim());
assert.equal("instructions" in completed.response, false);
assert.equal("tools" in completed.response, false);
// #8990 (commit c996dc93c2) deliberately stopped stripping `tools` from the
// TERMINAL snapshot — Codex CLI rebuilds its tool list from response.completed.
// stripResponsesLifecycleEcho still strips it on created/in_progress.
assert.deepEqual(completed.response.tools, [{ type: "function", name: "echoed_tool" }]);
assert.equal(completed.response.usage.total_tokens, 91);
});

View File

@@ -582,7 +582,10 @@ test("OpenAI -> Antigravity wraps Gemini requests in a Cloud Code envelope", ()
"model",
"userAgent",
"requestType",
// #9568: identity entries are emitted too (Gemini lowercases tool names in responses).
"_toolNameMap",
]);
assert.equal((result._toolNameMap as Map<string, string>).get("weather"), "weather");
assert.equal(result.userAgent, "antigravity");
assert.equal(result.requestType, "agent");
assert.match(result.requestId, /^agent\/\d+\/[0-9a-f]{8}$/);

View File

@@ -20,6 +20,11 @@ const { buildGeminiThoughtSignatureKey, storeGeminiThoughtSignature } =
type UnknownRecord = Record<string, unknown>;
/** Shape of the translated Gemini request the three finders below walk. */
interface GeminiRequestLike {
contents?: Array<{ parts?: UnknownRecord[] }>;
}
const CLAUDE_SIGNATURE_NAMESPACE = "regression-3440";
function seedClaudeThoughtSignature() {
@@ -29,7 +34,7 @@ function seedClaudeThoughtSignature() {
);
}
function findFunctionCall(result: any): UnknownRecord | undefined {
function findFunctionCall(result: GeminiRequestLike): UnknownRecord | undefined {
for (const content of result.contents ?? []) {
for (const part of content.parts ?? []) {
if (part?.functionCall) return part.functionCall as UnknownRecord;
@@ -38,7 +43,7 @@ function findFunctionCall(result: any): UnknownRecord | undefined {
return undefined;
}
function findFunctionCallPart(result: any): UnknownRecord | undefined {
function findFunctionCallPart(result: GeminiRequestLike): UnknownRecord | undefined {
for (const content of result.contents ?? []) {
for (const part of content.parts ?? []) {
if (part?.functionCall) return part as UnknownRecord;
@@ -47,7 +52,7 @@ function findFunctionCallPart(result: any): UnknownRecord | undefined {
return undefined;
}
function findFunctionResponse(result: any): UnknownRecord | undefined {
function findFunctionResponse(result: GeminiRequestLike): UnknownRecord | undefined {
for (const content of result.contents ?? []) {
for (const part of content.parts ?? []) {
if (part?.functionResponse) return part.functionResponse as UnknownRecord;

View File

@@ -36,17 +36,21 @@ function imageBody() {
}
describe("#7237 vision-capable models keep their images through compression", () => {
it("documents the drift: the conservative id-fragment heuristic disagrees with the authoritative spec for gpt-5.5", () => {
assert.equal(
isVisionModelId("gpt-5.5"),
false,
"the fragment-list heuristic has no gpt-5.x entry — it is a deliberately conservative fallback, not the source of truth"
);
it("the id-fragment heuristic now agrees with the authoritative spec for gpt-5.5", () => {
// Originally this case documented a DRIFT: the fragment list had no gpt-5.x
// entry, so the heuristic said false while modelSpecs said true. Commit
// 68cb678780 (enable vision flags for CC models) added the "gpt-5" fragment
// and closed that gap, so the two sources now converge. The invariant worth
// guarding is the AGREEMENT plus the fact that modelSpecs stays authoritative.
assert.equal(isVisionModelId("gpt-5.5"), true);
assert.equal(
getResolvedModelCapabilities({ model: "gpt-5.5" }).supportsVision,
true,
"modelSpecs.ts registers gpt-5.5 with supportsVision:true — this is the authoritative source chatCore must use"
);
// The heuristic stays deliberately conservative for ids it does not know;
// chatCore must still read the authoritative capability, never this fallback.
assert.equal(isVisionModelId("some-unknown-text-only-model"), false);
});
it("replaceImageUrls preserves the image when fed the authoritative capability (the fixed chatCore.ts:1330 behavior)", () => {
@@ -59,13 +63,16 @@ describe("#7237 vision-capable models keep their images through compression", ()
assert.equal(content[0].type, "image_url", "the block must remain a real image_url block");
});
it("regresses the pre-fix bug: feeding the raw heuristic value strips the image for gpt-5.5", () => {
const buggyValue = isVisionModelId("gpt-5.5"); // false — the pre-fix chatCore.ts:1330 input
const result = replaceImageUrls(imageBody(), { supportsVision: buggyValue });
it("reproduces the bug SHAPE: a false supportsVision strips the image to a placeholder", () => {
// The original case derived the wrong value from isVisionModelId("gpt-5.5").
// That no longer returns false (68cb678780), so the false is now supplied
// directly — what this guards is the stripping behaviour itself, which is
// exactly why chatCore must pass the authoritative capability and not a guess.
const result = replaceImageUrls(imageBody(), { supportsVision: false });
assert.equal(
result.applied,
true,
"sanity check: this reproduces the bug shape when fed the wrong (heuristic) value"
"sanity check: this reproduces the bug shape when fed the wrong value"
);
});

View File

@@ -14,6 +14,9 @@ export default defineConfig({
"open-sse/services/autoCombo/__tests__/**/*.test.ts",
"open-sse/services/combo/__tests__/**/*.test.ts",
"open-sse/services/__tests__/antigravity-quota-family.test.ts",
// #8890 shipped this suite into a directory no runner collects, so it had
// never executed once (check:test-discovery flags it as a NEW orphan).
"open-sse/services/__tests__/fail-fast-concurrency-gate.test.ts",
"src/lib/memory/__tests__/generic-backend.test.ts",
"tests/unit/autoCombo/**/*.test.ts",
"tests/unit/encryption.spec.ts",