mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-12 02:02:13 +03:00
fbbef4eaaf8b705f62c032c0dc952f5dfd58ddb5
69 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
493cae2ed9 |
Merge remote-tracking branch 'origin/release/v3.8.50' into codex/wave3b-9424
# Conflicts: # config/quality/file-size-baseline.json # src/i18n/messages/en.json # src/i18n/messages/pt-BR.json # src/shared/constants/featureFlagDefinitions.ts # stryker.conf.json |
||
|
|
c6195cec62 | fix(security): harden Adobe credential parsing gates | ||
|
|
2d49f1c743 |
maint: follow-up cherry-pick fix-in-place #9833 (conflict-resolved fallback) (#9899)
* fix(nvidia): keep 410 failures model-scoped * test: register NVIDIA 410 regression for mutation coverage * chore: preserve Stryker config formatting * fix(auth): reduce file size Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Zartharas <1402357+Zartharas@users.noreply.github.com> |
||
|
|
8b29e73b27 |
Merge remote-tracking branch 'origin/release/v3.8.50' into fix/release-v3.8.50-base-reds-9737-final
# Conflicts: # tests/unit/adobe-firefly.test.ts # tests/unit/claude-code-rendering-fixes.test.ts # tests/unit/telemetry-auto-cleanup-6848.test.ts # tests/unit/triage-bugs-2026-08-02.test.ts |
||
|
|
b7864cdb6c | fix(ci): clear coverage pack and unit gate regressions | ||
|
|
77cce62357 |
fix(ci): restore current release test integrity (#9819)
* fix(ci): restore base test integrity * fix(quality): clear inherited fast gate drift * fix(ci): clear inherited unit test reds * fix(i18n): preserve Vietnamese catalog parity * fix(i18n): preserve Portuguese catalog parity --------- Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> |
||
|
|
36abd86929 |
fix(ci): clear the 08-08 base-red layers — dead-code, prod crash in chat.ts, Responses payload regression, born-red stdio test, gate drifts (#9757)
* fix(ci): drop unused RadarReferrals type export — dead-code ratchet back to 227 baseline The radar referral-links feature (#9697) exported the inferred type RadarReferrals from feedSchema.ts but nothing imports it (the singular RadarReferral is the consumed type). knip counts it as a new dead export, pushing the dead-code ratchet to 228 > 227 and failing Fast Quality Gates on every PR born after the merge. RadarReferralsSchema itself stays — it is used by RadarFeedSchema. Refs #9737 * fix(ci): clear the 08-08 base-red layer — prod crash in chat.ts, Responses API payload regression, born-red stdio test, gate drifts Six independent base-reds from the 08-07 evening merge batch, each verified against the pure release/v3.8.50 tip: - src/sse/handlers/chat.ts: #9467's squash carried a refactor hunk that renamed the all-rate-limited breaker guard to an UNDEFINED variable (isAllRateLimited) — a production ReferenceError on the all-accounts-429 path (chat.ts is outside typecheck:core scope, so only tests caught it). Restore credentials?.allRateLimited. Guard: chat-rate-limit-body-lock (2/2), also un-breaks batch_api and chat-combo-live-test. - open-sse/utils/stream.ts: #9315 switched providerPayload summaries to the accumulated responseBody, but in passthrough paths that body is synthesized in chat-completion shape — Responses API lost its `response` object in the dashboard payload. Keep the events-derived summary for OPENAI_RESPONSES only. Guard: stream-utils + stream-collector-9315 suites (51/51). - tests/unit/mcp-stdio-json-purity.test.ts: born red — the full CLI chain takes ~10s (2x tsx import + DB init) and the test slept a fixed 4s. Poll for the first stdout line with a 60s deadline instead. - tests/unit/plugins-route-error-sanitization.test.ts: register #9445's new marketplace/install route in PLUGIN_ROUTES (route already sanitizes) (33/33). - tests/unit/provider-models-route-codex.test.ts: realign pinned GPT-5.6 input limit to #9432's deliberate 272000→922000 bump (7/7). - lint: fix 11 no-explicit-any errors in repro-9630 + specialty-9293 tests, prune 1 orphaned suppression, allowlist the opencode-ai devDependency (#8869, publisher-verified), and reword a doc line the fabricated-docs gate misread as an env var. Gates re-verified locally: lint:json --max-warnings 0 exit 0, dead-code 227, typecheck:core clean, check:deps OK, check:fabricated-docs OK. Refs #9737 * fix(ci): clear the third 08-08 base-red layer — invalid ru rule pack, stale event pin, orphaned UI repro test, pack/mutation/file-size drifts Follow-up to the previous layer: the serial fast-gates chain unmasked one more stratum after file-size/dead-code went green, all verified against the merged release/v3.8.50 tip: - compression rules ru/ultra.json (#9581): two rules shipped minIntensity "notes", which is not a valid CavemanIntensity (lite|full|ultra) — loading ANY language pack list threw and killed the rtk-loader suite. Mapped both to "ultra" (they are the most aggressive punctuation/case rules, matching the en pack tiers). 2/2. - plugins-welcome-banner-e2e: #9668 added the onStreamComplete builtin event (real emission path via runOnStreamCompleteHooks) and missed this pinned-list sibling. 35/35. - tests/unit/free-pool-frontend-repro (#9046): landed as .tsx with node:test semantics — no runner collects tests/unit/*.tsx, so it NEVER ran (test-discovery NEW-orphan). It contains zero JSX; renamed to .test.ts so the unit runner's existing glob collects it. 5/5 (first real run). - pack-policy: allow + require bin/mcpStdioConsoleGuard.mjs (#9281) — it is preloaded via node --import by bin/mcp-server.mjs, so a published artifact without it crashes 'omniroute --mcp' at startup. - stryker.conf.json: add 5 covering unit tests from the batch (#8779/#9204/ #9330/#9630/openrouter-passthrough) to tap.testFiles (--strict drift). - file-size-baseline: consolidate the base-drift rebaseline for the 12 files grown by the 08-06..08-08 batches (#9616's entries never reached the base; measured on this branch's tree — this PR's own source edits add zero lines to any frozen file). Local battery: file-size/deps/test-discovery/mutation/pack-policy/dead-code/ duplication/docs-all/secrets/vuln/workflows ratchets all exit 0; full lint gate --max-warnings 0 exit 0. Refs #9737 * fix(types): clear the 3 uncovered open-sse-typecheck regressions + realign combo skip-code siblings Fourth base-red layer unmasked by the serial gates. The other 4 typecheck regressions (codex.ts, kiro.ts, tierResolver.test.ts, translator/index.ts) already have dedicated open [TS7] PRs (#9748/#9753/#9742/#9747) — not duplicated here. This commit covers only what no open PR owns: - devin-agentic/serializer.ts TS2367: drop the dead 'role === "system"' branch — the guard above already narrows role to user|assistant (system throws unsupported_role). Devin suites 104/104. - raycast.ts TS2416: the buildHeaders 'override' never matched the base signature (2nd param is the signed payload string, not the stream boolean) — renamed to a private buildRaycastRequestHeaders helper so a polymorphic buildHeaders(credentials, true) call can never bind here. - modelMetadataRegistry.ts TS2352: PricingByProvider → nested-record cast now goes through unknown (shape is runtime-guarded by findInsensitive). - combo-routing-engine.test.ts: realign 2 pre-dispatch-skip expectations to #9630's deliberate ALL_TARGETS_SKIPPED contract (87/87). Refs #9737 * fix(ci): clear the fifth 08-08 base-red layer — reasoning-placeholder contract sweep, GPT-5.6 limits sweep, vi key parity The 08-08 merges (#9610 reasoning replay, #9432 GPT-5.6 limits, #9630 combo skip codes, #9336 provider key links) each changed a contract and left sibling tests pinning the old one. Full grep sweep per contract, not just the shard that happened to go red: - reasoning placeholder (#9573/#9610): the fix DELIBERATELY removed NON_ANTHROPIC_THINKING_PLACEHOLDER injection on cache miss — the model echoed the placeholder as its own reasoning (empty stop) and re-poisoned cache + client history; DeepSeek's 400 is specific to an EMPTY STRING, not an absent field. Realigned reasoning-cache (2 cases, renamed to describe omission) + tool-request-sanitization (1 case + dead import). 60/60. - GPT-5.6 Codex limits (#9432, 272000 -> 1050000 ctx / 922000 input): realigned vscode-token-routes-gpt56 (2) + vscode-token-routes (3). 43/43 together with t23-t24. - combo skip codes (#9630): t23-t24-fallback-resilience T24 now expects ALL_TARGETS_SKIPPED like the combo-routing-engine siblings. - vi.json key parity: #9336 added providers.getApiKey/getApiKeyDescription to en.json without syncing vi (the only locale with a parity gate). Translated both; providers block reordered to match en key order. 5/5. - pack-artifact-policy.test.ts: sibling of this PR's own required-paths change (bin/mcpStdioConsoleGuard.mjs). 10/10. - combo-routing-engine.test.ts: dropped the 6 comment lines added in the previous commit so the frozen test file-size stays at its baseline (the rationale lives in that commit message, not the test body). Gates: file-size, test-discovery, mutation-test-coverage, pack-policy, open-sse-typecheck, dead-code all exit 0. Refs #9737 * fix(translator): keep the reasoning_content placeholder for Xiaomi MiMo — #9610 traded one live 400 for another The xiaomi-mimo replay test (9router#1321) went red on the base after #9610 removed the NON_ANTHROPIC_THINKING_PLACEHOLDER injection globally. That test is NOT stale — it guards a documented upstream 400 ('Param Incorrect: The reasoning_content in the thinking mode must be passed back to the API'), so realigning it would have masked a reintroduced production bug. Two real bugs conflict here: - #9573: forwarding the placeholder makes the model continue its chain of thought FROM that text (echo -> empty stop) and re-poisons cache/history. - 9router#1321/#1337: omitting reasoning_content on a plain replay turn makes Xiaomi MiMo reject the request outright. #9610's evidence for omitting is provider-specific — it verified that deepseek-v4-flash accepts an ABSENT field. It does not extend to MiMo. So the omission stays for every provider #9610 covered, and the placeholder survives the cache miss only for xiaomi-mimo (new requiresReasoningContentPresence predicate next to isReasoningOnlyReplayTarget). The echo that comes back is still stripped on the way in by isInternalReasoningPlaceholder(), so #9573's cache/history poisoning stays fixed for MiMo too. Both contracts now hold simultaneously: xiaomi-mimo replay + reasoning-cache + tool-request-sanitization 61/61; placeholder-strip/responses/translator/combo regression sweep 168/168. Gates: file-size, open-sse-typecheck, dead-code, mutation-test-coverage exit 0; typecheck:core clean. A live check on the VPS (Hard Rule #18 path 2) is the only way to confirm the DeepSeek half of #9610's empirical claim; flagging it in the PR rather than widening this fix on speculation. Refs #9737 * test(translator): pin the reasoning-placeholder provider scope so neither half of the conflict can silently re-break #9610 removed the placeholder globally on the strength of ONE provider's observed behavior (deepseek-v4-flash accepting an absent reasoning_content), which re-opened the MiMo 400 (9router#1321). The previous commit scoped the placeholder to xiaomi-mimo; this pins BOTH directions in one test so the next global edit fails loudly instead of trading the bugs again: - xiaomi-mimo plain replay turn, cache miss -> reasoning_content present (narrowing the scope away from MiMo re-opens 9router#1321) - deepseek plain replay turn, cache miss -> reasoning_content absent (widening it back to DeepSeek re-opens the #9573 echo bug) Guard verified by mutation: forcing requiresReasoningContentPresence() to return true makes the DeepSeek half fail (1 pass / 1 fail), and the file was restored from the pre-probe copy before committing. Also checked kimi-coding/kimi-coding-apikey, the other strict-contract entries in REASONING_REPLAY_PROVIDERS: their originating PR (#7673) fixes capture and replay of REAL reasoning and documents no 400 on an absent field, so they stay out of the placeholder scope — evidence-scoped, not speculatively widened. Reasoning suites together: 87/87. Gates: file-size, test-discovery, mutation-test-coverage, dead-code exit 0; eslint clean. Refs #9737 --------- Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> |
||
|
|
348102a114 |
feat(api): add response content encoding verification (#6736) (#9444)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679). |
||
|
|
c40d4b17ee |
fix(ci): clear the NEW base-reds from the 08-06 merge batch (migration collision #2 + broken import) (#9688)
* test(base): realign six suites with contracts that #9100/#8990/#9009 deliberately changed Continuing the base-red drain — every one of these reproduces on the pure tip. - tests/snapshots/provider/translate-path.json: regenerated via UPDATE_GOLDEN=1. The diff is ADDITION-ONLY — the unorouter block from #9009; no existing provider entry changed. 3/3. - tests/unit/provider-models-route.test.ts: |
||
|
|
5ea43c7a9d |
feat: make forwarded upstream response-header budget configurable (#9243) (#9492)
Validated in local merge-train (diegosouzapw batch) |
||
|
|
04683029a6 |
fix(build): exec native esbuild binary directly in prepublish (dast-smoke base-red) (#9558)
* fix(build): exec native tool binaries directly in runBuildTool #8858 routed every resolved local bin through process.execPath to avoid Windows .cmd shims — but esbuild >=0.25 ships bin/esbuild as the NATIVE platform executable (ELF on Linux), so Node parsed machine code as JS and build:cli died with 'SyntaxError: Invalid or unexpected token', turning dast-smoke red for every PR. runBuildTool now sniffs the entry's magic bytes (ELF / Mach-O / PE) and execs native binaries directly; JS entries keep going through this Node binary (the .cmd-shim avoidance #8858 wanted). Validation (RED->GREEN on this box): - RED: node node_modules/esbuild/bin/esbuild --version -> SyntaxError (ELF) - GREEN: the exact failing CI step reproduced via the new logic bundles open-sse/mcp-server/server.ts successfully (4.2MB output, 1.3s). * fix(docs): add MDX frontmatter to the 20 remaining docs without it Same failure class as AGENTROUTER_WAF (#9503) and DOCKER_RELEASE_CHANNELS (this run's dast-smoke red): any doc without frontmatter breaks the fumadocs MDX loader during next build, killing build:cli/dast-smoke for every PR. Swept ALL of docs/ (i18n mirrors excluded) in one pass so this class cannot recur one file at a time. * docs(env): document OMNIROUTE_INTERNAL_SERVICE_TOKEN(+_FILE), OPENROUTER_PROVIDER_STATS_* and embedded-Redis binding vars Pre-existing env/docs contract drift from recently merged features made check:env-doc-sync red for any docs-touching PR. Values and defaults read from the defining modules (internalServiceAuth.ts, openrouterProviderStats.ts). * fix(build): resolve bundled npm-cli.js in the standard Unix layout + safe npm fallback off-Windows The opencode-plugin step hard-failed on GitHub runners because resolveBundledNpmEntry only looked next to the node binary (Windows zip layout); hostedtoolcache Node keeps npm at <prefix>/lib/node_modules/npm. Added that candidate, and when neither exists on non-Windows the step now falls back to plain 'npm' — the .cmd-shim hazard #8858 avoids is Windows-only. * test(mutation): register xai-agent-tools-passthrough.test.ts in stryker tap.testFiles The test landed on release/v3.8.50 covering open-sse/handlers/chatCore/passthroughHelpers.ts without the stryker registration, so Fast Quality Gates' drift detection reds any PR that carries it. Mechanical registration so its mutant kills count. --------- Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> |
||
|
|
4dbbaeb746 |
test(mutation): register capability-filter.test.ts in stryker tap.testFiles
The mutation test-coverage drift gate (check:mutation-test-coverage --strict) failed because tests/unit/capability-filter.test.ts covers open-sse/utils/error.ts (a mutated module) but was missing from stryker.conf.json tap.testFiles. |
||
|
|
2c966c28af | test(mutation): include adaptive admission coverage | ||
|
|
7b8055c7f8 |
fix(resilience): count STREAM_EARLY_EOF as a provider failure in combo routing (#9251)
* fix(resilience): count STREAM_EARLY_EOF as a provider failure in combo routing A STREAM_EARLY_EOF is an upstream that accepted the request (HTTP 200), opened the SSE stream, then closed it without emitting a single non-ping event. The combo path classified it together with STREAM_READINESS_TIMEOUT through isStreamReadinessFailureErrorBody(), and the readiness exemption in shouldRecordProviderBreakerFailure meant the whole-provider circuit breaker never saw it. During a provider-wide outage that makes the breaker blind. Over a 7-day window on our router we recorded 311 of these events, 302 of them on one model, 265 inside the upstream's published incident window — and the provider breaker sat at CLOSED / failure_count=0 the entire time. Every request kept being dispatched to the failing provider instead of shedding to the next combo target. The two codes are different signals. The readiness probe is a pre-flight liveness check on a connection we have not committed to, so failing it means "this connection looks stale". An early EOF means the provider took the request and then failed to serve it. The single-model path already treats it that way: shouldTripProviderBreakerForResult has no readiness exemption, so a 502 early EOF trips the breaker there. This makes the combo path consistent. isStreamReadinessFailureErrorBody keeps matching both codes, because the transient-retry and round-robin semaphore-cooldown paths in combo.ts do want identical treatment for both. Only the breaker needs to tell them apart, so the distinction is added as a narrow predicate and an optional argument rather than by changing the shared classifier. Omitting the new argument reproduces the previous behaviour exactly. Follows the additive-override pattern established by the isProxyUnreachable work, and leaves the existing exclusions for client aborts and plain 429s untouched. * test: register stream-early-eof-breaker in stryker tap.testFiles The mutation test-coverage gate (check:mutation-test-coverage --strict) detects unit tests that cover a mutated module but are missing from stryker.conf.json tap.testFiles, so their mutant kills would not count. comboPredicates.ts is one of the mutated modules, and the new stream-early-eof-breaker.test.ts covers it, so the gate correctly flagged the omission. 8376-econnrefused-breaker.test.ts -- the test this one is modeled on -- is already registered; this just brings the new file in line. No production code change. --------- Co-authored-by: Nick Sullivan <nick@technick.ai> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
a8216c92fe |
fix(sse): preserve error-only stream diagnostics (#9022)
* fix(sse): preserve error-only stream diagnostics * test(ci): register stream readiness mutation coverage * chore(changelog): finalize PR 9022 fragment |
||
|
|
d6c06932ec |
chore(quality): re-pin ceilings after merge-train 3 + register CF-1010 test
file-size: apiKeys.ts 1518 -> 1529 (#8805, treating cx/* and codex/* as equivalent provider prefixes in API-key model permissions) and chatCore.ts 5006 -> 5020 (#8806, passing the real response payload into plugin onResponse hooks instead of a hardcoded {status:200}). Both extend existing call sites rather than adding a branch. stryker: account-fallback-cf1010-no-retry-8775.test.ts covers accountFallback.ts but was missing from tap.testFiles, which would redden Fast Quality Gates on every subsequent PR. |
||
|
|
f8487648c8 | fix(resilience): keep resource 404s from cooling models (#8756) | ||
|
|
cd7a492984 |
chore(quality): re-pin file-size ceilings after merge-train 1H + fix stryker drift
file-size: nine frozen entries could not absorb the combined result of the
31-PR train. Two distinct causes, kept apart in the baseline note on purpose:
(1) GENUINE irreducible growth at existing chokepoints —
providerLimits/auth (#8632), rateLimitManager (#8616),
models-catalog-route.test (#8610).
(2) COLLISION with #8585, which banked shrinks measured on the pre-train
release tip while 30 sibling PRs in the SAME train grew those files
again — chat/accountFallback (#8628), chatCore (#8613),
videoGeneration (#8581), imageGeneration.
Ceilings re-pinned to the post-merge tip. #8612 (also in this train) automates
shrink-banking so this self-inflicted drift stops recurring.
stryker: three covering unit tests were missing from tap.testFiles —
isLocalStreamLifecycleError-abort-shape (circuitBreaker.ts, a shared base-red
that was reddening Fast Quality Gates on every open PR),
noauth-autocombo-lockout-7623 (accountFallback.ts) and
kimi-quota-reset-recovery (auth.ts), the latter two landed with this train.
|
||
|
|
1d44ee00f8 |
refactor(sse): extract combo dispatch prelude into combo/dispatchPrelude.ts (#8582)
* refactor(sse): extract combo dispatch prelude into combo/dispatchPrelude.ts Pure move, no behaviour change. First of ~7 PRs decomposing the combo.ts god-file (#3501). handleComboChat evaluates a series of dispatch branches before it ever reaches target resolution or the sequential attempt loop. None of them iterate targets in priority order or need the failover/retry/credential gate machinery that follows, so they move to a leaf: - context-cache pin routing (Fix #679), including the pinIsDurablyUnhealthy / isPinnedModelDurablyUnhealthy health gate - fusion panel dispatch + the #6455 misconfiguration warn - pipeline chaining - nested combo-ref execute-mode runtime-unit dispatch Only the chaos and round-robin hand-offs stay inline (11 and 13 lines); extracting those would be pure indirection. open-sse/services/combo.ts 3642 -> 3341 (-301) open-sse/services/combo/dispatchPrelude.ts: 619 (under the 800 cap) Each helper keeps the fall-through protocol the inline blocks had: return a Response to OWN the request, return null to fall through. A flipped null/Response would silently bypass the whole combo strategy, so the new tests pin both directions for every branch. combo.ts re-exports pinIsDurablyUnhealthy so combo-pin-health-gate.test.ts keeps resolving. The leaf takes handleComboChat as a `runCombo` parameter instead of importing it, so combo/ keeps zero back-edges into combo.ts. Complexity-neutral: the first cut added +3 violations (two max-lines-per-function, one complexity) inside the new leaf, so evaluatePinnedResponse, orderRuntimeUnits, recordRuntimeUnitStickySuccess and buildBaseOptions were split out. check:complexity now measures 2169 and check:cognitive-complexity 956 — identical to the pristine base. * test(sse): close the dispatch-prelude coverage holes found by mutation testing An adversarial mutation audit of the suite added in the previous commit found it guarded the fall-through protocol well but asserted almost nothing about what the helpers do once they OWN the request. 5 of 12 seeded mutations survived. Worst case: deleting the pinned-model dispatch call outright left all 12 tests green. Three holes, now closed (8 tests -> 20): Hole A — the honored-pin path had zero coverage. Both existing pin tests DROP the pin, so the dispatch, the 200-but-empty quality gate, the [408, 429, 500, 502, 503, 504] failover list and the catch(pinErr) branch were unguarded — exactly the logic the 2026-06-21 / 2026-06-22 incident comments call load-bearing. Adds five tests over a seeded healthy provider connection so the pin is actually honored. Hole B — orderRuntimeUnits was only ever driven with `priority`, which is a no-op through it. Four of five strategy branches could be deleted with nothing failing. Adds round-robin rotation and weighted sticky ordering tests. Hole C — recordRuntimeUnitStickySuccess never did anything under test: both its guards need weighted/round-robin, so an early return changed nothing. Covered by the new sticky-batch test. Verified by re-running the mutations rather than assuming: all 7 that previously survived (delete-pin-dispatch, serve-despite-failed-quality, never-fail-over-on-transient, rr-counter-not-advanced, rotation-removed, weighted-sticky-skipped, sticky-recording-no-op) are now killed. The first sticky-batch test I wrote was itself vacuous — asserting "same unit twice" holds equally when the recording helper is stubbed out, since nothing advances the counter either. It now asserts the batch runs out and rotation resumes on the third dispatch, which is what actually distinguishes the two. Also restores API_KEY_SECRET in test.after; it was set at module load and never put back, inconsistent with the DATA_DIR handling beside it. * fix(ci): teach known-symbols gate the relocated fusion/pipeline dispatch The combo sub-check of check:known-symbols asserts every canonical routing strategy has a real dispatch branch. It scanned a hardcoded file list and matched only `strategy === "..."`, so the prelude extraction tripped it twice: [combo] 2 estratégia(s) canônica(s) sem branch de despacho em combo.ts: ✗ fusion ✗ pipeline Both branches are still wired — they just moved to combo/dispatchPrelude.ts and took the early-return guard form `if (strategy !== "fusion") return null;` that extracting a branch into a `tryXDispatch()` leaf naturally produces. Two changes, both extending existing precedent (the list already carries the Block J leaves for the same reason): - register combo/dispatchPrelude.ts in comboDispatchFiles - widen the extractor to `strategy [!=]== "..."` so the inverted guard counts Loose `==`/`!=` stay rejected, and no `handledNotCanonical` fallout: the gate now reports 20 canonical strategies, all 20 via despacho. * chore(ci): register combo-dispatch-prelude test in stryker tap.testFiles check:mutation-test-coverage --strict failed once the known-symbols fix let Fast Quality Gates advance to it: ✗ 2 covering unit test(s) across 2 module(s) are missing from stryker.conf.json tap.testFiles open-sse/services/combo/comboStructure.ts open-sse/services/combo/rrState.ts The new tests/unit/combo-dispatch-prelude.test.ts exercises both modules, and both are already in stryker's mutate list, so without the registration its mutant kills would not have counted toward the nightly mutation gate. Note (unchanged, still out of scope): combo/dispatchPrelude.ts itself is not in stryker's `mutate` list. Adding it would widen the nightly mutation surface, which is a separate call from fixing this drift. * docs(changelog): add fragment for #8582 combo dispatch prelude |
||
|
|
5be61dbcd6 |
feat(sse): relay upstream 4xx error bodies verbatim on the Anthropic request path (#8622)
* feat(sse): selective upstream 4xx error passthrough util * feat(sse): relay upstream 4xx error bodies verbatim on the Anthropic request path createErrorResult() gains an opt-in 7th param opts.passthrough; when set and the upstream body is eligible (per shouldPassthroughUpstreamError), the returned response body is the upstream 4xx body verbatim instead of the sanitized wrapper. Internal classification fields (error/rawMessage/ errorType/errorCode) are never affected. Wired into chatCore.ts's 7 upstream-error createErrorResult call sites (model_unavailable / context_overflow / generic upstream-error branches), gated on sourceFormat === FORMATS.CLAUDE so only /v1/messages requests get the verbatim body — OpenAI-format paths are unchanged. * test(quality): register upstream-error-passthrough in stryker tap.testFiles `tests/unit/upstream-error-passthrough.test.ts` covers `open-sse/utils/error.ts`, an instrumented module, so `check:mutation-test-coverage --strict` requires it in `tap.testFiles` — the gate flagged the drift on this PR. |
||
|
|
a096116f08 |
fix(quality): register the #8494 covering test in stryker tap.testFiles (#8692)
`tests/unit/8488-capability-filter-fail-closed.test.ts` landed with #8494 and covers `open-sse/services/combo/comboStructure.ts`, an instrumented module, but was never added to `tap.testFiles`. The `check:mutation-test-coverage --strict` gate therefore fails on `release/v3.8.49` itself, turning the Fast Quality Gates job red on every PR cut from it. Registering the file restores the gate (verified: "No drift") and makes that test's mutant kills count toward the module's mutation score. |
||
|
|
c06cd83022 |
fix(sse): shallow per-target copy for the combo attempt body (#7847) (#8553)
* fix(sse): shallow per-target copy for the combo attempt body (#7847) combo.ts deep-cloned the request body for every target. On a 3.05 MiB agent request that is 9.53 MiB at 3 targets, and it scales linearly: 3 targets 9.53 MiB (3.12x wire) -> ~0.001 MiB 5 targets 15.89 MiB (5.19x wire) -> ~0.000 MiB 10 targets 31.78 MiB (10.39x wire) -> ~0.001 MiB The isolation it bought only ever needed to contain TOP-LEVEL SCALAR writes. The full mutation surface on this path is two assignments: combo.ts bodyRecord.max_tokens = ... (reasoning buffer) chatCore.ts body.model = model (Background Task Redirection T41) Nothing mutates the nested payload; applyCompression and injectUniversalHandoffBody both return new objects (verified empirically -- neither touches its input, and both tolerate a frozen one). So a fresh top-level object per target gives identical isolation while sharing the expensive messages/tools arrays. Also fixes a REAL cross-target leak in handleRoundRobinCombo. It already used a shallow copy, but took it only when the reasoning buffer actually changed max_tokens -- every other attempt shared the caller's object outright. The new test reproduces it on the unmodified code: target 2 received model "mutated-by-openai/gpt-4o-mini". In production that is a Background Task Redirection on one round-robin target rewriting body.model for the next. The copy is now unconditional. The invariant is pinned by tests rather than by a comment listing mutation sites, so the clone strategy can change again without anyone re-deriving them by hand: - a target's in-place write must not leak into the next (priority / fill-first / round-robin; the stub reproduces chatCore's body.model write) - the caller's body is never mutated - the per-target copy stays shallow (targets share one messages array) - freeze probe: combo's own body handling performs no in-place writes * test(sse): register the combo attempt-body isolation test in stryker tap.testFiles The new test resets the circuit breaker in beforeEach, so it counts as a covering test for src/shared/utils/circuitBreaker.ts. Without registering it, check:mutation-test-coverage --strict reported a 4th drift entry that was not there on the pristine tip -- new drift introduced by this PR. Registered in sorted position; the gate is back to the 3 pre-existing entries (accountFallback.ts, error.ts, comboPredicates.ts) that #8538 addresses. |
||
|
|
cc63ac9f53 |
test(sse): register #8396 and #8376 unit tests in stryker tap.testFiles (#8538)
* test(sse): register #8396 and #8376 unit tests in stryker tap.testFiles check:mutation-test-coverage fails on release/v3.8.49 at its own HEAD: two unit tests cover mutated modules but are absent from tap.testFiles, so their mutant kills do not count and the drift gate blocks every PR->release run. - tests/unit/8396-cooldown-429-cap.test.ts covers open-sse/services/accountFallback.ts (imports checkFallbackError) - tests/unit/8376-econnrefused-breaker.test.ts covers open-sse/services/combo/comboPredicates.ts (imports shouldRecordProviderBreakerFailure) Both inserted in the array's existing sorted position; no other key touched. * test(sse): register repro-7503-no-choices in stryker tap.testFiles |
||
|
|
7a8f9156da |
test(sse): repair two base-red gates on release/v3.8.49 (#8490)
* test(sse): repair two base-red gates on release/v3.8.49 `release/v3.8.49` is red at its own HEAD ( |
||
|
|
6602af7478 |
feat: narrow mcp:connect scope + per-key HTTP tool-scope binding (#7895) (#7967)
* feat: narrow mcp:connect scope + per-key HTTP tool-scope binding (#7895) Adds MCP_CONNECT_SCOPE ("mcp:connect"), a narrow additive API-key scope (kept out of MANAGEMENT_API_KEY_SCOPES, same precedent as SELF_USAGE_SCOPE) that authorizes ONLY the /api/mcp/ LOCAL_ONLY route-guard carve-out -- remote MCP-only callers no longer need broad manage/admin scope just to reach the transport routes. Scoped strictly to /api/mcp/; every other LOCAL_ONLY bypass prefix still requires hasManageScope() unchanged. Also resolves the caller's real api_keys.scopes over HTTP/SSE (httpAuthContext.ts::resolveMcpCallerAuthInfo) and passes it to the MCP SDK's transport.handleRequest(req, { authInfo }), so extra.authInfo.scopes reaching tool calls reflects the Bearer key's own scopes instead of the OMNIROUTE_MCP_SCOPES env fallback -- scopeEnforcement.ts already prioritized authInfo, it was simply unfed over HTTP. Does not flip the OMNIROUTE_MCP_ENFORCE_SCOPES default; stdio is unaffected (no per-caller identity, stays on the meta/env fallback chain). Closes #7895 * test(mcp): register mcp-connect-scope test in stryker tap.testFiles (#7895) |
||
|
|
f909b1d45e |
fix(resilience): don't cool down accounts or trip the breaker on client aborts (#7908)
* fix(resilience): don't cool down accounts or trip the breaker on client aborts When the caller drops the connection mid-stream, the in-flight request surfaces request_signal_aborted, "Client disconnected", or a DOM AbortError with no upstream status code. These shapes were counted as provider failures: the serving connection went into cooldown, the provider circuit breaker accrued failures, and healthy accounts ended up marked unavailable from client-side cancellations alone. Treat client aborts as local stream lifecycle events (#4602 policy): extend isLocalStreamLifecycleError() to recognize abort shapes and skip connection disable and breaker accounting for them. Genuine upstream failures (5xx/429/401) are still counted. Fixes #7907. * fix(resilience): guard the two remaining breaker-trip call sites against client aborts (#7907) PR #7908 correctly wired isLocalStreamLifecycleError() into shouldSkipConnDisable() and chatHelpers.ts's onStreamFailure, but two separate breaker._onFailure()-triggering call sites were purely status-code gated and never checked it, so a client-side abort (no upstream status, defaults to 502, error='request_signal_aborted') still tripped the whole-provider circuit breaker — the highest blast-radius of the 3 resilience mechanisms: - src/sse/handlers/chat.ts: the single-model, non-combo terminal-failure path called breaker._onFailure() directly, bypassing the isFailure option (which only applies inside breaker.execute()). Extracted the predicate into shouldTripProviderBreakerForResult() and added the missing isLocalStreamLifecycleError guard. - open-sse/services/combo/comboPredicates.ts::shouldRecordProviderBreakerFailure(), used by handleComboChat's executeTarget (open-sse/services/combo.ts), gained the same guard via a new optional `error` field. Added tests/unit/circuit-breaker-abort-provider-trip-7907.test.ts exercising both real predicates directly (not just the isolated isLocalStreamLifecycleError() helper) — confirmed red on the unfixed code (missing export) and green after the fix, alongside the existing #4602/#7908/combo-breaker-429 suites (24/24 pass, no regressions). file-size-baseline.json: +1 combo.ts (irreducible call-site wiring for the new `error` field) and +1 chatHelpers.ts (own growth from the PR's already-verified onStreamFailure guard, surfaced only now since fast-gates PR->release skip check:file-size). Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouzapw@users.noreply.github.com> Co-authored-by: insoln <insoln@ya.ru> * test(quality): register circuit-breaker abort tests in stryker tap.testFiles The two new tests (circuit-breaker-abort-provider-trip-7907, circuit-breaker-client-abort) import mutated modules (circuitBreaker.ts, comboPredicates.ts) but were not listed in stryker.conf.json tap.testFiles, tripping check:mutation-test-coverage --strict. Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouzapw@users.noreply.github.com> |
||
|
|
39ccfcf28c |
fix: parse Gemini 429 RetryInfo.retryDelay for model lockout (#7940) (#7961)
* fix(sse): parse Gemini 429 RetryInfo.retryDelay for model lockout (#7940) Gemini free-tier 429 bodies carry a short explicit retry hint -- error.details[].{"@type": google.rpc.RetryInfo, retryDelay: "26s"} plus a "Please retry in Ns." message -- but parseRetryFromErrorText only matched 'reset after'/'will reset after' text, so quotaResetHintMs came back null and recordModelLockoutFailure fell back to getMsUntilTomorrow() for quota_exhausted, locking the model out for ~19h instead of ~26s. parseRetryHintFromJsonBody (retryAfterJson.ts) now walks error.details[] for a google.rpc.RetryInfo entry and parses its retryDelay via a shared parseDelayString helper (moved out of accountFallback.ts so parseRetryAfterFromBody and the model-lockout path use the same grammar). parseRetryFromErrorText also gained a 'please retry in Ns' text fallback for bodies without a parseable details[] array. Both new paths are capped by a dedicated MAX_SHORT_RETRY_HINT_MS (24h), independent of the existing 30-day MAX_PROVIDER_COOLDOWN_MS, since RetryInfo/please-retry-in are short throttling hints, not long-lived quota resets like Antigravity's 160h. Regression test: tests/unit/bug-7940-gemini-retrydelay.test.ts (RED before the fix: parseRetryFromErrorText returned null and the resulting lockout was ~19h; GREEN after: ~26s). * chore(quality): register bug-7940-gemini-retrydelay test in stryker tap.testFiles |
||
|
|
a47ce51d4c |
fix(api): classify /api/acp/agents as loopback-only (#7948) (#7966)
* fix(api): classify /api/acp/agents as loopback-only (#7948) /api/acp/agents is spawn-capable (POST registers a client-chosen `binary` via src/lib/acp/registry.ts; GET / POST {action:"refresh"} runs detectInstalledAgents() -> execFileSync(probe.command, probe.args, { shell }) transitively) but was never added to LOCAL_ONLY_API_PREFIXES in src/server/authz/routeGuard.ts, unlike every sibling spawn-capable route (Hard Rules #15/#17). A leaked JWT via tunnel could reach the version-probe execFileSync path. Adds the prefix to the loopback gate plus a direct regression test (tests/unit/route-guard-acp-agents-local-only.test.ts) and an assertion in tests/unit/check-route-guard-membership.test.ts documenting why the existing source-scan subcheck cannot catch this class of gap (the spawn call is transitive via registry.ts, not in the route file itself). * chore(quality): register route-guard-acp-agents-local-only test in stryker tap.testFiles |
||
|
|
d3f8bbe555 | fix(sse): recover invalid Anthropic thinking signatures once (#7906) | ||
|
|
65e0aeda79 |
[Part 1/3]refactor(qwen): replace legacy Qwen Code and remove OAuth provider (#7866)
* refactor(cli): remove legacy Qwen Code integration * refactor(qwen): remove deprecated Qwen OAuth provider * feat(cli): rebuild Qwen Code integration for upstream V4 * fix(qwen): clear stale CLI auth on reset * test(qwen): align retired provider coverage * fix(db): renumber qwen-cleanup migration 129 -> 130 release/v3.8.49 tip took slot 129 via #7843 (usage_history_codex_strong_identity, itself renumbered from 128 during the #7838/#7840 base-red cleanup) after this branch forked; renumber remove_unregistered_qwen_data to 130. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
b2efa35982 |
fix(sse): CC bridge loses OpenAI-format image input (OpenCode/Kilo/Cline → AgentRouter) (#7888)
* fix(sse): convert OpenAI media parts to Claude blocks in the CC bridge OpenAI-format clients (OpenCode/Kilo/Cline) reach the Claude-Code-compatible bridge untranslated: chatCore skips the OpenAI->Claude translator when sourceFormat is OPENAI, so image_url / AI-SDK image / file parts either went upstream in OpenAI shape (silently ignored) or were dropped by the text-only extraction, and media-only user turns were removed by hasValidContent(). - claudeCodeCompatible: convertOpenAiMediaBlock() converts image_url (base64 + remote), AI-SDK string image and file parts (pdf->document, image mime->image) to Claude blocks in both bridge paths; Claude-native blocks pass through unchanged and the text-only wire image is preserved. - claudeHelper: hasValidContent() now counts image/document blocks so media-only user turns are not silently deleted. Reported-by: beingshafin Refs #7777 * refactor(sse): extract CC media-block conversion to ccOpenAiMediaBlocks.ts claudeCodeCompatible.ts is frozen at 1202 lines by check:file-size; the #7777 helpers pushed it to 1291. Move them to a dedicated module, no behavior change. * test(quality): register cc-bridge-openai-image-7777 test in stryker tap.testFiles |
||
|
|
3ba3cd145e |
chore(quality): fix release-tip base-reds — providerTierField case collision + stryker 7806 registration
(1) #7838 added providerTierField.ts next to ProviderTierField.tsx in the same directory — a case-only collision that breaks webpack on case-insensitive filesystems; the #6584 guard fails Unit shard 4/4 on every fresh PR run. Rename the helper to providerTierFieldApi.ts (import + test path adjusted). (2) check:mutation-test-coverage --strict fails on the tip because merged #7806's combo-skip-conn-disable-plugin-block test was never registered in stryker tap.testFiles. Register it. |
||
|
|
0fb5fa66bc | fix(quality): register nvidia-quota-phase1 and service-provider-plugin-registry in stryker tap.testFiles (#7796) | ||
|
|
07b2cf9b7e |
test(ci): register 5 covering unit tests in stryker tap.testFiles (base-red unblock)
Clears the release base-red where account-fallback-lockout-eviction, cliproxyapi-dedicated-credential-7645, combo-least-used-account, combo/recovery-hint and route-guard-forge-jcode-settings-local-only were covering mutated modules but missing from tap.testFiles. |
||
|
|
ea650253af |
Stream model health probes for slow providers (#7377)
* fix(model-test): stream slow chat probes * fix(model-test): handle JSON responses for streaming probes * fix(model-test): preserve transient errors from streaming probes * fix(model-test): keep transient probe failures visible * chore(ci): rerun pull request checks * docs(model-test): clarify transient failure handling * chore(ci): rerun pull request checks * test(model-test): cover slow timeout response path * chore(quality): register base-branch mutation tests * fix(sse): stop real-network leak and DOMException crash in model-test-runner timeout path Two new tests added by this PR fail against the current release tip: - tests/unit/model-test-runner.test.ts's slow-timeout regression test races the cold-start cost of the chat-completions pipeline (SSE translators, compression settings, etc. all lazily init on the first real request in a process). With a 1s AbortController timeout, the abort can fire before chatCore ever reaches the executor's fetch() call; the mocked fetch is then invoked after the test's own `finally` block has already restored the real fetch, so the assertion on the mock never fires and the request leaks onto the real network. Warm up the pipeline with one fast, resolving mock call before timing the 1s scenario. - Once the warm-up unblocks that race, a second, real bug surfaces: withRateLimit's abort handling (open-sse/services/rateLimitManager.ts) mutates `reason.name = "AbortError"` in place. When `AbortController.abort()` is called with no explicit reason (as modelTestRunner's timeout path does), the default reason is a native DOMException, whose `name` is a read-only getter — the mutation throws `TypeError: Cannot set property name of [object DOMException] which has only a getter` instead of rejecting cleanly. Build a fresh Error instead of mutating the caller-supplied reason, preserving the original as `.cause`. Adds a focused regression test in tests/unit/rate-limit-manager.test.ts that reproduces the DOMException crash directly against withRateLimit. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
a5e5e88092 |
fix: DDG circuit breaker (#6999) + null content validation (#7000) (#7001)
* feat(6922): register effort-tier aliases for glm-5.2 & mimo-v2.5 on opencode-go Previously only deepseek-v4-pro had effort-tier aliases on the opencode-go provider. GLM-5.2 and MiMo-V2.5 only had base model ids, making it impossible to pin reasoning effort per combo target. Changes: - Generalize parseDeepSeekEffortLevel → parseEffortLevel with EFFORT_TIERS table - deepseek-v4-pro: low/medium/high/max (unchanged) - glm-5.2: high/max only (OpenAI transport; low/medium not supported) - mimo-v2.5: high/max only (same reasoning) - Register alias model ids in opencode-go registry - Mark base models supportsReasoning: true - 9 unit tests covering registry + executor + backward compat Closes #6922 * ci: retrigger CI for Electron Package Smoke flaky test * ci: retrigger flaky Electron Package Smoke * test(#6922): rewrite tests to call real parseEffortLevel function - Export parseEffortLevel from opencode.ts so tests can import it - Replace grep-on-source-file assertions with real function calls - 13 tests: 4 deepseek tiers + 2 glm-5.2 tiers + 2 mimo-v2.5 tiers + 5 negative cases (unknown model, unsupported tiers, empty, base-only) - Remove dependency on readFileSync / string matching * fix: DDG circuit breaker (#6999) + null content validation (#7000) #6999: Add lightweight circuit breaker to DuckDuckGo executor. After 5 consecutive failures (429, 5xx, network errors), the breaker opens for 30s — during that window every request fast-fails with 503 so the combo engine can immediately fail over to the next provider instead of waiting for timeouts. Half-open probing happens naturally once the cooldown expires. A single success resets the counter. #7000: Fix false positive in validateResponseQuality where multimodal content arrays (empty []) and whitespace-only strings passed as valid. Now properly validates: arrays must have >=1 non-empty part; strings must have non-zero trimmed length. * test: add regression tests for DDG circuit breaker (#6999) and null content validation (#7000) - Circuit breaker: verifies 400 for empty messages is unaffected by CB state, and that CB starts closed (no 503 on first request) - Null content (#7000): verifies validateResponseQuality correctly flags null content, empty array content [] as invalid, and array with text as valid * fix(ci): add ddg-circuit-breaker test to stryker tap.testFiles for mutation coverage gate * test(#6999): exercise the DDG circuit breaker state machine directly The existing "circuit breaker fast-fails with 503 after consecutive failures" test never actually drives 5 consecutive failures — it makes a single real network call and only asserts the response isn't 503, which passes whether or not the breaker logic works at all (confirmed by disabling the open-threshold check entirely: that test stayed green). Exports cbIsOpen/cbRecordFailure/cbRecordSuccess/CB_THRESHOLD/ CB_COOLDOWN_MS (previously module-private) plus two test-only helpers (__setDdgCircuitBreakerStateForTests/__getDdgCircuitBreakerStateForTests, following the __xxxForTests convention already used in src/shared/utils/circuitBreaker.ts) so tests can drive the module-level singleton directly instead of needing a full network mock through warmSession/seedChallengeChain/acquireAuthHeaders, and without waiting CB_COOLDOWN_MS=30s in real time for the half-open case. New tests cover: starts closed; opens on the CB_THRESHOLD-th consecutive failure (not before); execute() fast-fails with 503 while open without reaching the network (verified: disabling the cbIsOpen() gate makes the same test fall through to a real network call, ~1s slower and red); still open just before cooldown elapses; self-closes once cooldown has elapsed (half-open); cbRecordSuccess resets the counter. Red-first proof (both independently green->red->restored-green): 1. `if (false && failures >= CB_THRESHOLD ...)` — neuters the open transition. Result: the new "opens after CB_THRESHOLD..." test fails; the pre-existing weak test stays green regardless. 2. `if (false && cbIsOpen())` — neuters the execute() gate. Result: the new "execute() fast-fails with 503 while open" test fails (and takes ~1s longer, falling through to a real network attempt instead of short-circuiting). Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
b6de89fae4 |
chore(quality): register #6672 test in stryker tap.testFiles (base-red unblock) (#7652)
Validated in local merge-train @ 8f27177d1 (full parity suite green: typecheck+file-size+complexity+cognitive+changelog+unit shards 1&2+vitest) |
||
|
|
6695cbbf7a |
feat: add EdgeTTS audio-tts provider (#6668) (#7605)
* feat(sse): add EdgeTTS audio-tts provider (#6668) Registers Microsoft Edge "Read Aloud" as a new no-API-key AUDIO_SPEECH_PROVIDERS entry — the first WebSocket-transport TTS provider in the registry. Reverse- engineered/unofficial endpoint, same class of integration already accepted for other "-web" style providers (chatgpt-web.ts, copilot-web.ts). - open-sse/executors/edgeTts.ts: pure Sec-MS-GEC token construction (SHA-256 over a public trusted-client-token + rounded Windows file-time ticks, ported from rany2/edge-tts drm.py), WS message framing (speech.config/ssml), binary-chunk demuxing, SSML building/escaping, and the WS synth call itself (injectable WebSocket ctor for tests, lazy `import("ws")` in production so it never enters esbuild's top-level CJS bundle graph). Per-client-IP sliding-window throttle (SlidingWindowLimiter) since there's no per-user key — one abusive deployment could otherwise get the shared trusted token rate-limited for everyone. - open-sse/utils/publicCreds.ts: embeds the trusted-client-token via resolvePublicCred() (Hard Rule #11) — it's a constant hardcoded in every Edge build and every open-source edge-tts port, not a per-user secret. - Extracted open-sse/utils/audioResponse.ts (shared response helpers) and open-sse/executors/awsPollyTts.ts (AWS Polly handler) out of open-sse/handlers/audioSpeech.ts to stay under its frozen file-size ratchet baseline while making room for the new branch — no behavior change to either extracted piece. - src/app/api/v1/audio/speech/route.ts: thread the caller's IP through to the handler for the new throttle. Tests: tests/unit/edgetts-provider.test.ts (23 cases) — Sec-MS-GEC determinism and cross-check against a hand-derived reference vector, message framing, binary demux, SSML escaping/injection-safety, registry lookup, publicCreds shape, and the error path via an injected fake WebSocket (upstream failure -> sanitized 502, no stack/path leak; Hard Rule #12), plus the per-IP rate limit. No live upstream is required or used — the reverse-engineered protocol can't be validated against real credentials, but every pure/testable seam is covered per the TDD path in the bug/feature validation gate. * test(mutation): register edgetts-provider.test.ts in stryker tap.testFiles (#6668) The new provider's unit test covers a mutated module, so the strict mutation-test-coverage gate requires it in stryker.conf.json's tap.testFiles. Single-line addition (kept the file's existing formatting). |
||
|
|
624aba2498 |
feat(cli): add Grok Build CLI tool setup (~/.grok/config.toml) (#7241)
* feat(cli): add Grok Build CLI tool setup (~/.grok/config.toml) Registers xAI's Grok Build TUI coding agent as a configurable CLI tool in /dashboard/cli-code, so OmniRoute can write itself in as a custom model provider in ~/.grok/config.toml. Mechanism: Grok Build reads a TOML config that can hold several user-defined [model.*] sections plus a [models].default pointer. Unlike the sibling Forge handler (which owns its whole config file and can full-replace it), this one surgically upserts ONLY the [model.omniroute] section and rewrites [models].default, leaving every other section byte-intact. Apply records the previous default in an `# omniroute-prev-default` marker comment so Reset can restore the user's original default instead of guessing. Built on OmniRoute's existing CLI-tools infrastructure rather than replaying the upstream shape: getCliRuntimeStatus() for detection (no ad-hoc `which grok` exec), Zod validation via cliModelConfigSchema, the write guard, createBackup(), the cliToolState DB module, and sanitizeErrorMessage() for every error path (Hard Rule #12). Security: GET reaches getCliRuntimeStatus(), which spawns a child process to locate and healthcheck the `grok` binary. That is the same transitive-spawn surface that classified /api/skills/collect/, so the route is registered in LOCAL_ONLY_API_PREFIXES and loopback-enforced before any auth check (Hard Rules #15 + #17). Writing a local CLI's config file is inherently a local-machine operation, so this costs no real capability. Co-authored-by: rixzkiye <rizkiyemubarok05@gmail.com> Inspired-by: https://github.com/decolua/9router/pull/2571 * chore(changelog): fragment for #7241 * fix(cli): shrink cliTools.ts/cliRuntime.ts under the file-size ratchet + fix stale catalog counts The grok-build registry/runtime entries pushed cliTools.ts (916->932) and cliRuntime.ts (1128->1137) past their frozen file-size caps. Extract the grok-build entries into cliToolsGrokBuild.ts (registry, typed) and cliRuntimeGrokBuild.ts (runtime metadata, deliberately untyped/no cliCatalog import so it doesn't drag that schema file into the typecheck:core curated allowlist's transitive graph). The amp runtime entry rides along in the same runtime file for the extra headroom needed to clear cliRuntime.ts's cap with zero slack. Also update the two catalog-cardinality canaries (cli-tools-schema.test.ts, cli-catalog-counts.test.ts) and EXPECTED_CODE_COUNT to include grok-build: 20->21 visible code entries, 24->25 total code entries, 32->33 grand total. Fixes CI reds on #7241 surviving a release/v3.8.49 merge: Fast Quality Gates (check:file-size) and Unit Tests fast-path (1/4, 2/4). * test(stryker): register grok-build route-guard test in tap.testFiles check:mutation-test-coverage --strict flagged tests/unit/route-guard-grok-build-settings-local-only.test.ts as a covering unit test for src/server/authz/routeGuard.ts missing from stryker.conf.json's tap.testFiles allowlist (only became reachable once the Fast Quality Gates job got past the file-size fix earlier in this branch). --------- Co-authored-by: rixzkiye <rizkiyemubarok05@gmail.com> |
||
|
|
fce2bb67ad |
fix: recognize Ollama Cloud session usage-limit 429 as quota-exhausted (#7071) (#7181)
* fix: recognize Ollama Cloud session usage-limit 429 as quota-exhausted (#7071) Ollama Cloud's 5-hour "session" usage-limit 429 body ("you (NAME) have reached your session usage limit...") was never recognized as quota-exhausted -- only the sibling "weekly usage limit" wording was fixed (#6638/#3709). Neither the generic QUOTA_PATTERNS list nor the dedicated weekly-quota classifier matched the session wording, so checkFallbackError() fell through to the generic ~3s rate-limit backoff instead of a long QUOTA_EXHAUSTED cooldown -- combo/LKGP routing cycled back to the "exhausted" account almost immediately instead of advancing to the next one. Adds isSessionUsageLimitText()/buildSessionQuotaFallback() to quotaTextCooldowns.ts, mirroring the weekly-quota pair, with a 5h cooldown matching Ollama Cloud's documented session window. Wired unconditionally into checkFallbackError() next to the weekly check so apikey-category providers like ollama-cloud are covered. * chore(test): register issue-7071-ollama-session-quota.test.ts in stryker tap.testFiles (#7071) |
||
|
|
2a5f9a5ed7 |
fix(sse): flatten structured (array) content in Qwen Web executor (#6927)
* fix(sse): flatten structured (array) content in Qwen Web executor foldMessages did String(m.content), turning OpenAI-style content-part arrays into the literal "[object Object]" prompt. Add contentToText() to extract the text parts. Reported on the support mesh. TDD: red->green regression test tests/unit/qwen-web-content-array-serialization.test.ts * docs(changelog): add fragment for #6927 * fix(stryker): register qwen-web content-array test in tap.testFiles Fast Quality Gates flagged the new coverage for open-sse/executors/qwen-web.ts as missing from stryker.conf.json's tap.testFiles list. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
3164eafbd1 |
fix(providers): scope nvidia NIM 404s to the single failing model (#6773) (#6888)
* fix(providers): scope nvidia NIM 404s to the single failing model (#6773) The nvidia registry entry multiplexes 17 models from 9 different upstream vendors (z-ai/, minimaxai/, deepseek-ai/, qwen/, mistralai/, stepfun-ai/, moonshotai/, openai/, nvidia/) behind one connection, but was missing passthroughModels: true — unlike 34 other multi-model registries (modelscope, synthetic, kilo-gateway, etc). Without it, hasPerModelQuota returns false for nvidia, so a 404 on a single stale/renamed model falls through checkFallbackError's generic catch-all as a connection-wide cooldown instead of being scoped to just that model, poisoning all 17 nvidia models for the cooldown window. Add passthroughModels: true to the nvidia registry entry so 404/429s on one model lock out only that model. Regression test: tests/unit/nvidia-passthrough-models-6773.test.ts * fix(quality): register nvidia passthrough test in stryker tap.testFiles check-mutation-test-coverage.mjs --strict flagged tests/unit/nvidia-passthrough-models-6773.test.ts as an unregistered covering test for accountFallback.ts (Fast Quality Gates). Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com> |
||
|
|
0f3c68fa69 |
fix(sse): de-flake timing-sensitive combo cooldown/breaker tests (#6803) (#6897)
* fix(sse): de-flake timing-sensitive combo cooldown/breaker tests (#6803) Extracts 3 wall-clock-sensitive assertions (combo-quota-share cooldown ceiling x2, circuit-breaker HALF_OPEN race) into tests/unit/serial/ (--test-concurrency=1, the repo's established remedy for this class of test) and widens their margins, since a starved CI-runner event loop can blow even a serialized test's timing window. Also adds an explicit 30s vitest timeout to the MCP audit shutdown test, which had no override and inherited vitest's 5000ms default. Regression proof: reproduced RED locally under real devbox CPU contention (2644ms/1796ms elapsed vs the old 1500ms ceiling, exactly the reported failure mode); confirmed GREEN after the fix under the same contention. * fix(quality): register new serial timing tests + prune stale any-suppression count - stryker.conf.json: add tests/unit/serial/combo-quota-share-cooldown-wait-timing.test.ts and tests/unit/serial/combo-strategy-fallbacks-half-open-timing.test.ts to tap.testFiles so their mutant kills count for accountFallback.ts and circuitBreaker.ts (PR #6897 added these files but didn't register them). - eslint-suppressions.json: combo-strategy-fallbacks.test.ts's no-explicit-any suppression count was stale (35) after this PR trimmed 2 any-usages out of the file when extracting the half-open timing test; corrected to 33. Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com> |
||
|
|
8c4364a597 |
fix(providers): give v0-vercel-web its own alias so credentials are detected (#6343) (#6891)
* fix(providers): give v0-vercel-web its own alias so credentials are detected (#6343) * test(6343): type casts to satisfy no-explicit-any gate * test(6343): register v0-web + cliproxyapi tests in stryker tap.testFiles The two unit tests added on this branch cover mutated modules (src/sse/services/auth.ts, comboContextCache.ts) but were missing from stryker.conf.json tap.testFiles, tripping check:mutation-test-coverage --strict. Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> |
||
|
|
5097e3a111 |
fix(api): route error responses through sanitizeErrorMessage (Hard Rule #12) (#6886)
* fix(api): route error responses through sanitizeErrorMessage (Hard Rule #12) 9 API routes returned raw String(error)/error.message directly in HTTP 500 bodies, leaking SQLite paths, SQL text and internal messages. Route all through sanitizeErrorMessage() per Hard Rule #12: - settings/compression (GET+PUT), settings/compression/mcp-accessibility (GET+PUT) - cache/entries (GET+POST), db/health (GET+POST), db-backups/exportAll - assess, combos/test, settings/notion, settings/obsidian Test: tests/unit/rule12-error-sanitization-sweep.test.ts asserts sanitized 500 bodies contain no absolute paths / stack tails. * test(stryker): register rule12 error-sanitization sweep in tap.testFiles The new tests/unit/rule12-error-sanitization-sweep.test.ts covers a mutated module, so it must be listed in stryker.conf.json tap.testFiles for the mutation-coverage gate (check-mutation-test-coverage.mjs --strict) to pass. Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com> --------- Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
993f280c4b |
fix(ci): publish electron-updater latest.yml manifests in release assets (#6766) (#6881)
* fix(ci): publish electron-updater latest.yml manifests in release assets (#6766) * fix(ci): register new mutation-covering test + realign stale codex-cli version fixture - stryker.conf.json: add tests/unit/cliproxyapi-model-mapping-dispatch.test.ts to tap.testFiles so it counts toward mutation coverage for the newly-added comboContextCache.ts coverage (check:mutation-test-coverage --strict was failing). - tests/unit/provider-models-route-codex.test.ts: DEFAULT_CODEX_CLIENT_VERSION was bumped to 0.144.0 on release/v3.8.47 after this test's fixtures were written; realign the hardcoded 0.142.0 expectations to the current constant. |
||
|
|
47bc3317c1 |
fix(oauth): embed Trae OAuth client_id via resolvePublicCred (Hard Rule #11) (#6870)
* fix(oauth): embed Trae OAuth client_id via resolvePublicCred (Hard Rule #11) * fix(quality): register tests/unit/trae-publiccred.test.ts in stryker tap.testFiles The mutation test-coverage gate (check:mutation-test-coverage --strict) flags new covering unit tests that mutate open-sse/utils/publicCreds.ts but aren't listed in stryker.conf.json's tap.testFiles, so their mutant kills wouldn't count. Register the new test file alphabetically. Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com> --------- Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
d256fc367a |
fix(sse): stop combo path tripping whole-provider breaker on plain 429 (#6868)
* fix(sse): stop combo path tripping whole-provider breaker on plain 429 The combo path recorded a whole-provider circuit-breaker failure for a plain rate-limit 429, opening the breaker after N consecutive 429s and blocking every account+model on that provider. This contradicts the single-model path and the documented RESILIENCE_GUIDE policy. - Single-model path uses PROVIDER_BREAKER_FAILURE_STATUSES = Set([408, 500, 502, 503, 504]) (src/sse/handlers/chat.ts:206) — 429 excluded. - Combo path gated shouldRecordProviderBreakerFailure on isProviderFailureCode (accountFallback.ts), whose PROVIDER_FAILURE_ERROR_CODES INCLUDES 429 for connection-cooldown scope — so a plain 429 wrongly tripped the whole-provider breaker. Fix scopes tightly: comboPredicates now tests a local PROVIDER_BREAKER_FAILURE_STATUSES set mirroring the single-model constant (429 excluded), instead of isProviderFailureCode. The shared isProviderFailureCode / PROVIDER_FAILURE_ERROR_CODES are deliberately left untouched — they drive connection-cooldown / model-lockout logic where 429 must still count. A genuine quota/token-limit terminal 429 is handled elsewhere; only the whole-provider breaker-recording gate changes. Adds tests/unit/combo-breaker-429.test.ts covering the 429 exclusion, the 408/5xx inclusion, and the sameProviderNext / skipProviderBreaker suppression paths. * test(quality): register combo-breaker-429.test.ts in stryker tap.testFiles Fast Quality Gates' mutation-coverage drift check flagged this PR's new covering test for comboPredicates.ts as unregistered. Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com> --------- Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
1f9ac3628b |
fix(sse): combo model lockout honors parsed upstream quota reset (#6863) (#6866)
* fix(sse): combo model lockout honors parsed upstream quota reset (#6863) The combo failure path recorded model lockouts from checkFallbackError's cooldownMs only, discarding quotaResetHintMs — the ungated channel that carries a parsed upstream quota reset (e.g. Antigravity 429 "Resets in 92h27m28s"). With OAuth profiles defaulting useUpstreamRetryHints=false, the lockout fell back to the base cooldown (seconds), so quota-dead accounts were re-walked serially by every combo request for days (measured 122s per request, 494s worst case in #6863). Thread max(cooldownMs, quotaResetHintMs) into selectLockoutCooldownMs at both combo lockout sites, mirroring the single-model path pattern in src/sse/services/auth.ts (v3.8.43). All three resilience fences are preserved: useUpstreamRetryHints still gates connection cooldowns, the hint only affects model-scope lockouts, and combo 429s remain non-persistent. TDD: tests/unit/combo-lockout-quota-reset-6863.test.ts fails on base (lockout 5000ms) and passes with the fix (~92.5h). * test(sse): tighten #6863 lockout assertion to parsed-reset bounds; prettier pass Assert remainingMs falls within (parsedResetMs - 5s, parsedResetMs] so a hardcoded long cooldown cannot satisfy the regression test. Also apply Prettier to both changed files (includes one pre-existing formatting fix in handleRoundRobinCombo picked up by --write). * fix(sse): align combo lockout hint selection with single-model path; register test in mutation gate Adopt review feedback: replace Math.max(cooldownMs, quotaResetHintMs) with the auth.ts pattern (usedUpstreamRetryHint ? cooldownMs : quotaResetHintMs) so a parsed reset SHORTER than the fallback cooldown wins too — e.g. the subscription-quota branch returns a 1h fallback while the body says "resets in 45m"; max() would over-lock by 15 minutes. Add a regression test for the short-reset case (fails against the max() variant) and register the new test file in stryker.conf.json tap.testFiles to satisfy check:mutation-test-coverage --strict. * chore(quality): register cliproxyapi dispatch test in mutation gate tests/unit/cliproxyapi-model-mapping-dispatch.test.ts landed on release/v3.8.47 via #6903 without a tap.testFiles entry, so check:mutation-test-coverage --strict fails on the branch tip and on every PR merge ref. Register it so the gate is green again. --------- Co-authored-by: judy459 <JUDYZHU459@outlook.com> Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
1b7a9150e5 |
chore(ci): fix shared base-reds blocking PR queue (stryker registration + codex 0.144 test)
- register tests/unit/cliproxyapi-model-mapping-dispatch.test.ts in stryker.conf.json tap.testFiles (gap from #6903) - update provider-models-route-codex.test.ts client_version 0.142.0 -> 0.144.0 (stale test from #6780 prod bump) |