mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-07 07:42:13 +03:00
aa7b3913862cf2f32a5cd7f09c8ffb9be87c19d0
58 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
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) |
||
|
|
0a7b58b20f |
\ feat: operator-configurable account rotation\ (#6763)
* feat(resilience): operator-configurable account rotation Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy files belong to #6620, not this PR); the author's accountFallback/.env deltas were re-applied cleanly onto the release tip. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * docs(env): document configurable account-rotation env vars in ENVIRONMENT.md Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * refactor(rotation): extract rotation gate/context helpers to keep accountFallback.ts under frozen cap Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * chore(changelog): re-sync CHANGELOG.md to release tip (restore lost base bullet) Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * test(stryker): register rotation-config test in tap.testFiles for mutation coverage Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * test(stryker): register ollama-quota covering tests (drift from #6731/#6817/#6742) + re-sync Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> |
||
|
|
a42f993645 |
chore(stryker): register ollama-quota covering tests (merge-burst drift, v3.8.47)
The 3 covering unit tests from #6731/#6817/#6742 (issue-6638-ollama-quota, ollama-cloud-weekly-quota-cooldown-3709, issue-6686-quota-preflight-coverage) exist on release but were never added to tap.testFiles when those PRs merged. Completes the registration so mutant kills count; unblocks every PR touching a mutated module. Part of the owner-approved merge-burst drift cleanup. |
||
|
|
b9d18dd8c4 |
Continue fix bugs and upgrade skill_collector (#6294)
* fix(skills): gate skill-collector CLI detection behind management auth + loopback PR #6294 fork-main bundled genuinely new skill-collector CLI-detection routes (GET /api/skills/collect/detect, POST /api/skills/collect/install) on top of content already shipped via #6186. This reconstructs the PR against the current release tip, keeping only the new detect/install routes and their SKILL.md, and drops the 3 already-merged commits so two post-merge quality fixes on /api/github-skills (Zod validation + sanitizeErrorMessage) are not reverted. - GET /api/skills/collect/detect spawned a child process per CLI_TOOL_IDS entry via getCliRuntimeStatus(), unauthenticated and reachable over any tunnel. All 3 routes (github-skills GET/POST, skills/collect/detect, skills/collect/install) now require requireManagementAuth(), matching every sibling /api/skills/* route. - Classified /api/skills/collect/ in LOCAL_ONLY_API_PREFIXES and SPAWN_CAPABLE_PREFIXES (routeGuard.ts / spawnCapablePrefixes.ts) and added src/app/api/skills/collect to SPAWN_CAPABLE_ROUTE_ROOTS in check-route-guard-membership.ts so the automated gate actually scans it (Hard Rules #15 + #17). - omniroute_github_skills_install MCP tool now reports the honest action: "planned" instead of "installed", matching the REST route. - Dropped docker-compose.drive-d.yml, start.sh, and the unrelated @types/node/settings.ts changes (personal dev-machine / out-of-scope). - Added route-level tests for all 3 routes + the 3 MCP tools (auth-required and no-stack-trace-leak assertions) and a route-guard regression test. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(quality): register new routeGuard covering test in stryker.conf.json check:mutation-test-coverage --strict (Fast Quality Gates) flagged tests/unit/authz/route-guard-skills-collect.test.ts as a covering unit test for src/server/authz/routeGuard.ts that was missing from tap.testFiles, so its mutant kills would silently not count toward the mutation-test baseline. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * chore: resync CHANGELOG after merging release/v3.8.47 Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- 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: Moseyuh333 <Moseyuh333@users.noreply.github.com> |
||
|
|
6557f44bd2 |
feat(settings): 9router-style Routing Strategy card + sticky parity (#6678)
* feat(dashboard): 9router-parity Routing Strategy card + provider/combo sticky override (#6678) Add a Routing Strategy settings card (Settings -> Routing) surfacing account round-robin/sticky-limit knobs plus a new combo-level sticky round-robin (comboStickyRoundRobinLimit), and a per-provider account-routing override (providerStrategies) wired into getProviderCredentials() ahead of the global fallback strategy. Rebased onto release/v3.8.47 (credit-preserving reconstruction: unrelated package.json/electron/proxyDispatcher drift from the PR's stale base was dropped, only the author's own 12 files were re-applied). Split ProviderAccountRoutingCard/RoutingStrategyCard into smaller hook+subcomponent pieces to stay under the frozen complexity/file-size gates; rebaselined ProviderDetailPageClient.tsx/auth.ts's frozen file-size caps for the small additive growth. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * chore(quality): register combo-rr-sticky-9router.test.ts in stryker tap.testFiles (#6678) CI's Fast Quality Gates -> check:mutation-test-coverage --strict flagged the new test as missing from stryker.conf.json's tap.testFiles (it covers the mutated module open-sse/services/combo/rrState.ts). Adds the single entry, alphabetized next to the existing combo-rr-fallback-advance-948.test.ts. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(changelog): restore CHANGELOG bullets eaten by release sync Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(changelog): re-restore CHANGELOG bullet after further release sync Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(changelog): re-restore CHANGELOG bullet after further release sync Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- 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: SeaXen <SeaXen@users.noreply.github.com> |
||
|
|
237bd59173 |
fix(api): sanitize catch-block error.message in middleware/hooks routes (#6645)
* fix(api): sanitize catch-block error.message in middleware/hooks routes POST /api/middleware/hooks and PUT /api/middleware/hooks/[name] returned the raw error?.message in their 500 response bodies (Hard Rule #12), which could leak internal SQLite error text/paths on a DB failure. Both now route through sanitizeErrorMessage() from open-sse/utils/error.ts, matching the pattern already used elsewhere in the codebase. Regression guard: tests/unit/middleware-hooks-error-sanitization.test.ts Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * test(mutation): register middleware-hooks-error-sanitization in stryker tap.testFiles The mutation test-coverage gate (check:mutation-test-coverage --strict) flagged tests/unit/middleware-hooks-error-sanitization.test.ts as covering open-sse/utils/error.ts but missing from stryker.conf.json's tap.testFiles list. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(changelog): restore CHANGELOG bullets eaten by release sync Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(changelog): re-restore CHANGELOG bullet after further release sync Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> |
||
|
|
fd7e4c10e5 |
feat(compression): omniglyph engine (context-as-image, Fable 5 direct) — stack + single mode (#6556)
* feat(compression): dependência omniglyph (file:) + smoke de import * feat(compression): engine omniglyph — contexto-como-imagem com gates fail-closed * fix(compression): omniglyph adapter fail-open no transform (try/catch) * feat(compression): registra omniglyph no registry e catálogo (single mode, stackPriority 90) * feat(compression): modo único omniglyph (async), selecionar o modo é o enable * feat(compression): plumbing supportsVision + providerTransport até os engines * feat(compression): estimador de tokens image-aware — modo stacked mantém a saída do omniglyph * docs(compression): corrige comentário do prefixo base64 no decode PNG (64 chars) * feat(compression): registra omniglyph nas listas de modo/engine (db, combo, deriveDefaultPlan, mcp) * feat(dashboard): dedicated OmniGlyph engine screen (context-as-image) Adds a per-engine detail page at /dashboard/context/omniglyph, alongside the other compression engines in the sidebar. Four sections: the economics (measured savings), a REAL before→after (dense text vs the rendered PNG page, not a mockup), the fail-closed gate flow, and the enable control wired to /api/settings/compression (preview engine, off by default). Sidebar entry + i18n label across all locales. * chore(compression): consume published omniglyph@^1.0.0 from the npm registry Replaces the local file: dependency used during the preview phase — npm ci now resolves omniglyph from the registry with integrity, unblocking CI. * fix(compression): satisfy v3.8.47 quality gates for the omniglyph engine - dependency-allowlist: approve omniglyph (own package, published from diegosouzapw/OmniGlyph; supply-chain review done by the maintainer) - ladder maps (#6533 guard): rank omniglyph 80 (stackPriority 90, runs after every text engine) with expectedReductionFactor 0.35 (measured 0.23-0.33) - drop the two explicit any casts in omniglyph tests (no-explicit-any is error-level in tests since #6218) * chore(compression): rebaseline strategySelector for the omniglyph mode dispatch +18 lines of cohesive dispatch/type wiring at the existing mode chokepoints (sync no-op + async single-mode branch + providerTransport on the options types) — not extractable without hiding the dispatch boundary, mirroring the prior compression rebaselines. Also drops an unused eslint-disable directive in image-aware-tokens.test.ts (warning-level red under --max-warnings 0). * chore(quality): register inherited base tests in stryker tap.testFiles masked-200-exhaustion-fallback-6427 and headroom-codex-quota-snapshot-6379 arrived via the base merge without their stryker registration — check:mutation-test-coverage --strict requires covering tests to be listed. * refactor(compression): keep omniglyph wiring under the complexity gate - extract the async single-mode resolution to engines/omniglyphSingleMode.ts (runCompressionAsync was at complexity 17 after the mode branch; back <=15) - split OmniglyphContextPageClient into section components (was 161 lines in one function; every function now under the 80-line cap) - complexity baseline 2052->2053: the +1 is inherited base drift (the ratchet does not run on fast-path merges — same pattern as the v3.8.44/46 rebaselines); this PR's own code is measured complexity-net-zero * chore(quality): register 3 more inherited base tests in stryker tap.testFiles route-guard-middleware-local-only, combo-diagnostics-trace and idempotency-fusion-collision arrived via the latest base merge without their stryker registration (fast-path merges skip check:mutation-test-coverage). * chore(quality): cognitive-complexity baseline 883->884 (inherited base drift) check:cognitive-complexity measures 884 identically on the pristine origin/release/v3.8.47 tip and on this HEAD — the PR itself is cognitive-net-zero (single-mode resolution extracted to its own module, page client split into section components). Same inherited-drift pattern as the v3.8.4x release rebaselines. --------- Co-authored-by: diegosouzapw <diegosouzapw@devbox.local> |
||
|
|
9f66316c4e |
feat(quality): validate-release-green --full-ci — reproduce the ci.yml static gate set (P0) (#6583)
* feat(quality): validate-release-green --full-ci reproduces the ci.yml static gate set The curated HARD/DRIFT lists in validate-release-green were a hand-maintained subset — v3.8.46 leaked 11 static base-reds (route-validation:t06, docs-symbols, bundle-size --ratchet, test-masking, file-size, …) to the release PR because they live only in the ci.yml gate jobs, costing ~2h of layered CI. --full-ci reads ci.yml itself and runs every npm run check:* / lint from the lint / quality-gate / quality-extended / docs-sync-strict / pr-test-policy jobs (-- ratchet flags preserved; test-masking against GITHUB_BASE_REF=main; skips pr-evidence + codeql-ratchet which can't run in a local working-tree pre-flight). Reading from ci.yml keeps the set current as gates are added. Also wired into nightly-release-green so a static base-red opens a tracking issue the night it lands. Regression guard: +5 extractCiGates cases (18/18 pass). * chore(quality): add 3 covering tests to stryker tap.testFiles (pre-existing drift on release/v3.8.47) check:mutation-test-coverage (fast-gates) flagged 3 unit tests that cover mutated modules but were missing from stryker.conf.json tap.testFiles — pre-existing drift on release/v3.8.47, surfaced by this PR's CI. Adds codex-quota-selection-hydration (auth.ts), combo-roundrobin-compat-fallback-6238 (circuitBreaker.ts), and combo-rr-fallback-advance-948 (rrState.ts) so their mutant kills count. |
||
|
|
c0430342c6 |
test(ci): quarantine concurrency-sensitive flakes into a serial pass (#6347)
* test(ci): quarantine concurrency-sensitive flakes into a serial pass (tests/unit/serial/) glm-coding-plan-monthly-3580, quota-division-blocks and provider-health-autopilot fail under --test-concurrency>1 CPU contention but pass isolated (v3.8.45 release benchmark: this class cost ~28min re-runs per CI round). They now live in tests/unit/serial/ and run in a dedicated --test-concurrency=1 step appended to every runner (test:unit, :ci, :ci:shard, :fast, :shard:1/2, coverage:runner — sharded variants shard the serial pass too, so concurrent shard jobs never self-collide). Discovery + TIA gates know the new glob; stryker.conf.json path updated. Guard: tests/unit/test-serial-quarantine.test.ts (4 tests). * test(ci): quarantine combo-health-autopilot too (async logger writes after test end under load) Fresh evidence from this PR's own CI: all 3 subtests pass but an async log write lands after the test ends (ENOENT app.log -> uncaughtException) — same concurrency-flake family. Moved to tests/unit/serial/, imports adjusted, stryker path updated, guard test now asserts 4 quarantined files. |
||
|
|
ffe825b6b8 |
fix(quality): clear the 2 remaining heavy-gate reds on the release tip
- check:error-helper: githubSkillTools.ts (#6186 wave) built MCP install error results with raw err.message — routed through sanitizeErrorMessage() (Hard Rule #12; 19/19 agentSkillTools tests green) - check:mutation-test-coverage: tests/unit/combo-provider-cooldown-sibling.test.ts (#6216) was missing from stryker.conf tap.testFiles — added so its mutant kills count on nightly-mutation |
||
|
|
bb62c2a618 |
chore(release): parallel-cycle flow — sync-next-cycle script + Hard Rule #21 semantics (#6203)
Integrated into release/v3.8.45 |
||
|
|
1bda6c15dc |
Release v3.8.44 (#5925)
* fix(install): add pnpm-workspace.yaml allowBuilds + pnpm.json for pnpm 11+ pnpm 11 introduced ERR_PNPM_IGNORED_BUILDS for native addon packages. Without explicit allowBuilds approval, these packages silently skip build scripts and OmniRoute fails to start with missing native modules. Changes: - pnpm-workspace.yaml: Set allowBuilds=true for all 13 native addon packages (@parcel/watcher, @swc/core, better-sqlite3, core-js, esbuild, keytar, koffi, libxmljs2, onnxruntime-node, protobufjs, sharp, tls-client-node, unrs-resolver) - pnpm.json: Migrate onlyBuiltDependencies from package.json (deprecated field) to the new pnpm.json config file per pnpm 11 spec. Tested on: pnpm 11.9.0, Node 24, Windows 11. Fixes: pnpm install ERR_PNPM_IGNORED_BUILDS on fresh clone with pnpm 11. * chore(release): open v3.8.44 development cycle * test(security): parse Kimi Web URL host instead of substring match (CodeQL #689) (#5928) Alert js/incomplete-url-substring-sanitization: the Kimi Web executor test asserted result.url.includes("www.kimi.com"), which a hostile host like www.kimi.com.evil.net would also satisfy. Parse the URL and assert on the exact hostname (new URL(result.url).hostname === "www.kimi.com"), which is both a stronger check and clears the CodeQL warning. * refactor(translator): extract thinking-budget fitting from openai-to-claude (#5932) Extract the thinking-budget fitting cluster (fitThinkingToMaxTokens + private safeCapMaxOutputTokens + MIN_* constants) verbatim into the pure leaf openai-to-claude/thinkingBudget.ts. Host re-exports fitThinkingToMaxTokens so external importers keep working and imports it back for internal use. Host 822 -> 738 LOC (under the 800 cap). No behavior change: byte-identical bodies, public export set unchanged. Adds a split-guard test; all consumer tests stay green (translator-openai-to-claude, strip-empty, minimax-m3, passthrough). * chore(release): pipeline hardening — test-masking pre-flight gate + contributors/uncovered helpers (#5926) * chore(ci): add test-masking PR-context gate to release-green pre-flight Reproduce check:test-masking (vs origin/main) inside validate-release-green so non-allowlisted net-assert reductions surface in the local pre-flight instead of in a ~40-min CI layer on the release PR. run() now merges a per-gate opts.env so GITHUB_BASE_REF reaches the child. HARD gate; skipped under --quick. Context: v3.8.43 release cost 3 CI round-trips for PR-context gates (test-masking, file-size, pr-evidence) that check:release-green did not reproduce locally. * chore(release): add contributors generator + uncovered-commit reconciliation helpers - scripts/release/gen-contributors.mjs: reproducible `### 🙌 Contributors` table for a CHANGELOG version (parenthetical-group parser → accurate per-PR attribution, noise-handle denylist). v3.8.43 shipped without the section (a real miss) because it was hand-built. npm run release:contributors <version> [--inject]. - scripts/release/list-uncovered-commits.mjs: lists commits since the last tag with no CHANGELOG bullet (v3.8.43 had 123/176 uncovered at reconciliation start). Advisory, maintainer-side. npm run release:uncovered. - 20 unit tests (parenthetical attribution, noise exclusion, idempotent injection, coverage window). * chore(quality): absorb web-cookie-providers-new file-size drift from #5928 (base-red on release/v3.8.44) * refactor(translator): split openai-responses request translator into pure leaves (#5940) Extract the shared pure primitives and the chat->Responses direction out of the 894-line openai-responses.ts request translator: - openai-responses/helpers.ts: pure primitives (toRecord/toString/clampCallId/ normalizeVerbosity/etc + markers/regexes/JsonRecord), zero host imports - openai-responses/toResponses.ts: openaiToOpenAIResponsesRequest (chat->Responses), imports the helpers leaf Host keeps openaiResponsesToOpenAIRequest (Responses->chat, imported by production) plus both register() directions, and re-exports openaiToOpenAIResponsesRequest so external importers (tests) keep working. Host 894 -> 529 LOC (under the 800 cap). Verbatim bodies (multiset check: leaf A 54/54, leaf B 294 lines, fn1 intact), public export set unchanged, leaves never import the host (no cycle). Adds a split-guard test; all consumer tests stay green (responses-translation-fixes 37, verbosity 4, reasoning-effort 4, orphaned-tool-filter 8, empty-tool-name-loop 8, headroom-responses-format 3). * chore(ci): pr-evidence FAIL output tells you to push (body edit does not re-run the gate) (#5944) ci.yml ignores the 'edited' event, so adding the Evidence block to the PR body after a push does not re-run check:pr-evidence — you need another commit. The FAIL report now says so, at the exact place someone sees the red check. + 5 unit tests (classification + hint-on-fail / no-hint-on-pass). Decided against a separate edited-triggered workflow: pr-evidence is not a required check (no ruleset gates it; release PRs merge UNSTABLE, not BLOCKED), so the gap is cosmetic and the generate-release skill already puts Evidence in the body before the first push. * fix(providers): Perplexity Web emits real tool_calls in streaming mode (mirror chatgpt-web toolMode) (#5927) (#5937) Perplexity Web (Pro/Max) only converted <tool>{...}</tool> text into OpenAI tool_calls for non-streaming requests (hasTools && !stream). Streaming requests -- the default for agentic coding clients -- got the raw <tool> text as plain delta.content and never emitted a tool_calls SSE delta, so clients could not execute tools. Reuses the provider-agnostic buildToolModeResponse()/ toolCompletionToSseStream() helpers already shipped for chatgpt-web (#5240): when tools are requested, buffer the full completion and convert it into either a JSON completion or a terminal SSE replay carrying delta.tool_calls + finish_reason: tool_calls, regardless of the caller's stream flag. Extended buildToolModeResponse()'s idSeed to be caller-supplied (default 'cgpt', perplexity-web passes 'pplx') so tool_call ids stay provider-specific without duplicating the helper. Non-tool streaming is unchanged (still lives token-by-token via buildStreamingResponse). * fix(discovery): resolve duplicate /v1 paths and redirect aborts (#5904) Integrated into release/v3.8.44. Thanks @hamsa0x7 for diagnosing the doubled /v1 discovery path and the REDIRECT_BLOCKED probe-loop abort (#5899). De-scoped to the discovery fix (the #5903 session-affinity work is handled by #5943) and added Rule #18 regression guards. * docs(changelog): record #5926 + #5944 (release-pipeline hardening) under v3.8.44 Maintenance (#5952) * docs(claude): add Hard Rule #22 — cross-session safety (git stash + in-flight PRs) (#5955) Integrated into release/v3.8.44 — Hard Rule #22 (cross-session safety). * refactor(translator): extract pure helpers from response/openai-responses (#5949) Extract the 5 stateless helpers (normalizeToolName, stripEmptyOptionalToolArgs, normalizeOutputIndex, normalizeUpstreamFailure, extractResponsesReasoningSummaryText) verbatim into the pure leaf openai-responses/pureHelpers.ts (no stream state, no host import). Host imports them back and re-exports normalizeUpstreamFailure for external importers (tests). Host 1091 -> 1001 LOC. The stateful streaming core stays in the host (out of scope). Byte-identical bodies (multiset 73/73), no cycle. Adds a split-guard; consumer tests stay green (responses-translation-fixes 37, combo-param-validation-fallback-4519 5). * docs(compression): document upstream sync policy for RTK/Caveman engines (#5830) (#5948) Integrated into release/v3.8.44 — docs-only upstream sync policy for RTK/Caveman engines (closes #5830). All 7 checks green. * fix(sse): strip ANSI/VT100 codes from gemini-cli stream frames (#5934) Integrated into release/v3.8.44 — ReDoS-safe ANSI/VT100 strip for gemini-cli stream frames (port of upstream #2273, thanks @anki1kr). PR test green (5/5), file-size gate OK. * fix(translator): strict Anthropic content-block compliance in antigravity→openai request (#5935) Integrated into release/v3.8.44 — strict Anthropic content-block compliance in antigravity→openai (port upstream #2296). PR test green (9/9). UNSTABLE red is the pre-existing environmental setup-claude base-red (opencode-plugin dist not built in fast-path), not a regression from this PR. * fix(mcp): auto-recover stale streamable HTTP sessions on initialize (#5957) Integrated into release/v3.8.44 — MCP stale streamable-HTTP session auto-recovery (thanks @Chewji9875). * fix(providers): validate v0 Platform API keys via chats endpoint (#5954) Integrated into release/v3.8.44 — v0-vercel Platform API key validation (thanks @vittoroliveira-dev). * fix(api): relax provider-scoped chat completion validation (#5907) Integrated into release/v3.8.44 — relaxed provider-scoped chat validation + regression test (thanks @nickwizard). * fix(providers): strip /v1 unconditionally to avoid /v1/v1/models fetch error (#5899) (#5920) Integrated into release/v3.8.44 — unconditional /v1 strip in both models-discovery paths + regression test (thanks @anki1kr). * fix(resilience): per-window is_exhausted + honor quota-exhaustion preflight for priority combos (#5923) (#5941) Integrated into release/v3.8.44. * fix(resilience): honor active codex session affinity over per-request reset-aware re-scoring (#5903) (#5943) Integrated into release/v3.8.44. * fix(thinking): only inject redacted_thinking replay block when tool_use present and thinking enabled (#5945) (#5953) Integrated into release/v3.8.44. * feat(providers): add ClinePass API-key provider (#5942) Integrated into release/v3.8.44 — ClinePass API-key (BYOK) provider (port upstream 9router#2304, co-authored @adentdk). Validated locally: 16 clinepass tests green; fixed the APIKEY count 158→159 + translate-path golden snapshot (clinepass is a genuine new provider). Remaining UNSTABLE red is the pre-existing environmental setup-claude base-red (opencode-plugin dist not built in fast-path). Supersedes stub #5541. * feat(api): add /v1/ocr endpoint (Mistral OCR) + Mistral moderation (#5950) Integrated into release/v3.8.44 — /v1/ocr endpoint (Mistral OCR) + Mistral moderation (port upstream 9router#2064, co-authored @waguriagentic). Validated locally: 14 ocr-route tests + moderation/servicekind/endpoint-category suites green (CORS→Zod→handler + no-stack-leak assertion). Reds are inherited DRIFT only: cognitive-complexity ratchet (none from OCR files — pre-existing cycle drift, rebaselined at release) + environmental setup-claude base-red. * fix(codex): convert chat json schema to responses text format (#5933) Integrated into release/v3.8.44 — converts Chat Completions json_schema response_format → Responses API text.format on the Codex path, and preserves existing text.format through verbosity normalization. Base redirected main→release; the openai-responses.ts split that landed this cycle was reconciled by re-applying the delta onto openai-responses/toResponses.ts. Validated locally: 48 translator-openai-responses-req + 8 codex-verbosity tests green. Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> * feat(providers): add Claude Sonnet 5 support across the model pipeline (#5833) Integrated into release/v3.8.44 — wires claude-sonnet-5 end-to-end (registries, modelSpecs, pricing ×3, cost, Sonnet-family fallback, 1M-ctx, static models). Reconciled the add/add overlap with the already-merged #5796 (kept the PR's superset test with the family-fallback assertion). Validated locally: kiro-sonnet-5 + catalog + pricing/modelSpecs/fallback suites all green. Thanks @ggiak! Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> * feat(relay): gate bifrost auto routing by provider manifest (#5870) Integrated into release/v3.8.44 — gates Bifrost auto-routing by the provider plugin manifest (only manifest-eligible providers reach the sidecar; ineligible/unknown fall back to the TS path with explicit reasons). Superset of #5869 (carries the full manifest + registry + docs). Resolved an integration-test conflict in favor of the release (which already subsumes this PR's readiness/removeDirWithRetry improvements). Validated locally: 4 provider-plugin-manifest + 11 relay-routing-backend tests green. Thanks @KooshaPari! Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> * refactor(translator): extract pure message helpers from openai-to-kiro (852→751) (#5947) * refactor(translator): extract pure message helpers from openai-to-kiro Extract the pure tool/message helpers (parseToolInput, normalizeKiroToolSchema, serializeToolResultContent) verbatim into the leaf openai-to-kiro/messageHelpers.ts. The host imports them back for convertMessages. They were module-private, so the public export set is unchanged (no re-export needed). Host 852 -> 751 LOC. Byte-identical bodies (multiset 99/99), leaf has zero imports (no cycle). Adds a split-guard; consumer tests stay green (translator-openai-to-kiro 33, translator-ai-sdk-image-parts 3). * chore: re-trigger CI (stuck runner on 2/2 shard) * refactor(executors): extract pure prompt + composer helpers from cursor (#5960) Extract two pure clusters from the cursor executor into sibling leaves: - cursor/prompt.ts: isRecordLike + toolChoiceDirectiveLine + buildCursorOutputConstraints - cursor/composer.ts: composer thinking-as-content decoding (isComposerModel, visibleComposerContentFromThinking, composerReasoningRemainder + markers) Host imports both back for internal use and re-exports the 3 composer helpers for external importers (tests). Host 1576 -> 1451 LOC. Byte-identical bodies (verbatim multiset prompt 65/65, composer 32/32), leaves have zero imports (no cycle). Adds a split-guard; consumer tests stay green (cursor-composer-thinking, cursor-streaming, cursor-agent-tool-calls, translator-openai-to-cursor, cursor-agent-system-prompt). * refactor(executors): extract pure SSE-collect parsing from antigravity (#5962) Extract the pure SSE-payload -> collected-stream parser (AntigravityCollectedStream, stripZeroWidth, parseAntigravityTextualToolCall, addAntigravityTextualToolCall, processAntigravitySSEPayload/Text, flushAntigravitySSEText) verbatim into the leaf antigravity/sseCollect.ts. Host imports the helpers it uses and re-exports processAntigravitySSEPayload for external importers (tests). Host 1812 -> 1671 LOC. Byte-identical bodies (verbatim multiset 135/135), leaf does not import the host (no cycle). Credit/quota state, auth, and HTTP dispatch untouched. Adds a split-guard; consumer tests stay green (executor-agy 8, executor-antigravity 26, antigravity-sse-collect-socket-release, copilot-agent-antigravity-parity 6). * refactor(executors): extract pure model maps + resolvers from chatgpt-web (#5967) Extract the static model maps (MODEL_MAP, MODEL_FORCED_EFFORT, THINKING_CAPABLE_SLUGS) and the pure thinking-effort resolvers (isThinkingCapableModel, normalizeThinkingEffort, resolveThinkingEffort, ResolvedChatGptModel, resolveChatGptModel) verbatim into the pure leaf chatgpt-web/models.ts. Host imports the two resolvers it uses back. Host 3205 -> 3076 LOC. Byte-identical bodies (verbatim multiset 120/120), leaf has zero imports (no cycle). Auth/PoW/session/HTTP dispatch and all module caches untouched. Adds a split-guard; consumer tests stay green (chatgpt-web 86, chatgpt-web-tools-5240 4, chatgpt-web-sha3-boringssl-5531 5). * refactor(executors): decompose grok-web into pure tool/markup leaves (#5994) Extract the pure OpenAI<->Grok tool-translation, native-tool mapping, markup cleanup, and NDJSON stream types out of the 1872-line grok-web executor into 4 sibling leaves: - grok-web/types.ts: GrokStreamResponse/GrokStreamEvent (stream types) - grok-web/tool-bridge.ts: OpenAI<->Grok tool translation + registry + classifiers - grok-web/native-tools.ts: native-tool selection/scoring + native->OpenAI mapping - grok-web/text-cleanup.ts: Grok markup stripping + GrokMarkupFilter Layered, acyclic: types <- tool-bridge <- native-tools; text-cleanup <- types; host imports the leaves. All symbols module-private (no host re-export). Host 1872 -> 887 LOC. Byte-identical bodies (verbatim per-leaf), no cycle, all new leaves <= 800 cap (tool-bridge split at line 753 to stay under). Auth/cookie/TLS/HTTP dispatch untouched. Adds a split-guard; consumer tests stay green (grok-web 62, grok-cli-oauth 15, grok-cli-strip-params 2). * refactor(executors): extract pure quota parsing from codex (#5999) Extract the pure Codex quota-snapshot parsing + reset/cooldown scheduling (CodexQuotaSnapshot, parseCodexQuotaHeaders, getCodexResetTime, getCodexDualWindowCooldownMs) verbatim into the leaf codex/quota.ts. Host re-exports the 4 symbols so handlers/chatCore/codexQuota.ts + tests keep resolving. Host 1539 -> 1427 LOC. Byte-identical bodies (verbatim 98/98), leaf has zero imports (only Date, no cycle). WS transport, auth, HTTP dispatch untouched. Adds a split-guard; consumer tests stay green (executor-codex 40, codex-quota-fetcher 7, chatcore-codex-quota 5). * refactor(executors): extract pure stream formatters from deepseek-web (#6000) Extract the pure content/citation formatters (isThinkingModel, isSearchModel, cleanDeepSeekToken, formatStreamContent, DeepSeekSearchResult, appendSearchCitations) verbatim into the leaf deepseek-web/stream-format.ts. Host imports the 5 it uses back into transformSSE/collectSSEContent (cleanDeepSeekToken stays internal to the leaf). Host 1147 -> 1108 LOC. Byte-identical bodies (verbatim 34/34), leaf has zero imports (no cycle), all module-private (no re-export). PoW/auth/token-cache/HTTP dispatch untouched. Adds a split-guard; consumer tests stay green (deepseek-web 35, deepseek-web-rolling-window-2942 5, deepseek-web-tools-execute 3). * refactor(api): add validatedJsonBody helper (salvage #5075) (#5931) Fuses JSON body parsing + Zod validation into a single call that returns either type-narrowed data or a ready-to-return 400 NextResponse with the standard error envelope. Salvaged as the Tier 1 portable helper from the closed refactor PR #5075; the bulk route migration is intentionally not ported. Adds a focused 6-case regression test. Co-authored-by: KooshaPari <KooshaPari@users.noreply.github.com> * feat(qoder): drive PAT auth via qodercli, add dashboard quota, fix connection display (#5816) Integrated into release/v3.8.44 — Qoder PAT auth via qodercli binary + dashboard quota + dual-auth connection fix. Thanks @AgentKiller45 (co-author @judy459)! Validated locally (release-green on its own merits): lint 0, typecheck:core 0, 104 qoder/usage/UI tests green, file-size gate OK (owner-approved qoderCli.ts baseline-freeze 666→989), env-doc-sync fixed (documented QODER_CLI_CONFIG_DIR). The 2 remaining CI reds are INHERITED base-reds, not caused by this PR: (1) LEDGER-4 minimax-m3 supportsVision (minimax-m3 base + cline-pass/minimax-m3 from the already-merged #5942); (2) mutation-test-coverage missing 3 tests in stryker.conf (#5903/#5942/#5923). Both cleaned up separately. Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> * fix(providers): minimax-m3 supportsVision (LEDGER-4) + stryker tap.testFiles drift (#6012) Release-green cleanup — clears LEDGER-4 minimax-m3 supportsVision + stryker tap.testFiles drift base-reds. Validated locally. * fix(registry): flag cline-pass/minimax-m3 as multimodal (supportsVision) (#6003) The cline-pass provider's minimax-m3 entry was missing supportsVision, breaking the LEDGER-4 registry-consistency test (all minimax-m3 entries must set supportsVision to match lite.ts — minimax-m3 is multimodal). Every other minimax-m3 registry entry (trae, bazaarlink, cline, ollama-cloud, ...) already sets it. This was a base-red on release/v3.8.44 inherited by every open PR. Validated by the existing failing-then-passing guard tests/unit/review-reviews-v3814-fixes.test.ts (LEDGER-4). * refactor(executors): extract pure payload construction from claude-web (#6006) Extract the pure Claude-web payload types + transforms + default tools/style (ClaudeWebRequestPayload, ClaudeWebStreamChunk, DEFAULT_CLAUDE_MODEL, generateMessageUUIDs, getDefaultTools, getDefaultPersonalizedStyle, transformToClaude, transformFromClaude) verbatim into the leaf claude-web/payload.ts. Host imports the 3 it uses back (ClaudeWebRequestPayload type + the two transforms). Host 1056 -> 835 LOC. Byte-identical bodies (verbatim 149/149), leaf imports only randomUUID (no host import, no cycle), all module-private (no re-export). Cookie/auth/ Turnstile/TLS/HTTP dispatch untouched. Adds a split-guard; consumer tests stay green (claude-web 13, claude-web-auto-refresh 6). * refactor(executors): extract pure upstream-header helpers from base (#6008) Extract the pure upstream-header helpers (mergeUpstreamExtraHeaders, getCustomUserAgent, setUserAgentHeader, applyConfiguredUserAgent, isOpenAICompatibleEndpoint, stripStainlessHeadersForOpenAICompat) verbatim into the leaf base/headers.ts. base.ts is imported by ~18 executors, so the host re-exports all 6 to keep those import paths intact; it also imports the 4 it uses internally in the BaseExecutor class. The trivial JsonRecord type alias is redefined locally in the leaf to avoid a base<->leaf cycle. Host 1539 -> 1451 LOC. Byte-identical bodies (verbatim 78/78), leaf does not import the host (no cycle). typecheck:core validates all base importers still resolve via the re-export. Adds a split-guard; consumer tests stay green (executor-base-utils 22, executor-default-base 49, executor-strip-stainless-openai-compat 6, plus executor sanity via typecheck). * refactor(executors): extract pure wire protocol from perplexity-web (#6014) Extract the pure Perplexity wire protocol (consts, SSE stream types, SSE parsing, OpenAI<->Perplexity message translation, request/query builders, content extraction, sseChunk) verbatim into the leaf perplexity-web/protocol.ts. Host imports back the 10 symbols it uses; everything module-private (no re-export). Session cache, TLS fetch, auth, and the executor class stay in the host. Host 1028 -> 534 LOC. Byte-identical bodies (verbatim), leaf imports only randomUUID (no host import, no cycle). Adds a split-guard; consumer tests stay green (perplexity-web 26, streaming-tools-5927 2, tls-client 6, key-validation-models 2). * refactor(executors): extract pure URL normalizers from default (#6015) Extract the pure per-provider chat-URL normalizers (normalizeBailianMessagesUrl, normalizeDataRobotChatUrl, normalizeAzureAiChatUrl, normalizeWatsonxChatUrl, normalizeOciChatUrl, normalizeSapChatUrl, normalizeXiaomiMimoChatUrl, normalizeOpenAIChatUrl, getOpenRouterConnectionPreset) verbatim into the leaf default/urlNormalizers.ts. Host imports them back into buildUrl/transformRequest; the now-dead build*ChatUrl/normalizeBaseUrl imports move to the leaf. All module-private (no re-export). Host 864 -> 815 LOC (shrunk below its frozen baseline). Byte-identical bodies (verbatim 45/45), leaf does not import the host (no cycle). buildHeaders/execute/auth untouched. Adds a split-guard; consumer tests stay green (executor-default-base 49, anthropic-compatible-bearer 3, strip-client-metadata 3). * feat(webfetch): support self-hosted FireCrawl instances (#5793) Integrated into release/v3.8.44 — self-hosted FireCrawl support (FIRECRAWL_BASE_URL/FIRECRAWL_TIMEOUT_MS). Re-cut clean onto the release tip (branch was fossilized from a pre-v3.8.40 snapshot). Validated: 4 firecrawl tests green, env-doc-sync + docs-sync pass. UNSTABLE red is the inherited environmental setup-claude base-red. * feat(xai): register XaiExecutor with reasoning-effort suffix parsing (#5800) Integrated into release/v3.8.44 — XaiExecutor with reasoning-effort suffix parsing. Re-cut clean onto the release tip (branch was fossilized). Validated: 6 xai-executor tests green, provider-consistency OK, typecheck:core 0 errors, env-doc-sync in sync. UNSTABLE red is the inherited environmental setup-claude base-red. * feat(discovery): Phase 2 — reporter, /api/discovery/* routes (strict loopback-only) + dashboard UI (#5939) * feat(discovery): Phase 2 reporter — discoveryResults DB module + service wiring Adds src/lib/db/discoveryResults.ts (CRUD over the discovery_results table from migration 074) and wires the opt-in discovery service to persist and read findings through it: persistDiscoveryResult / getDiscoveryResults / getDiscoveryResultById / markVerified / deleteDiscoveryResult, with (provider, method, endpoint) upsert de-duplication. Re-exported from localDb. The service stays opt-in / default-off. The /api/discovery/* routes and the dashboard UI tab are intentionally deferred to Phase 2b — they need the local-only enforcement model (Hard Rules #15/#17 territory) decided first. TDD: tests/unit/db/discovery-results.test.ts (8 cases, DB + service delegation), isolated DATA_DIR with resetDbInstance cleanup. * feat(discovery): Phase 2b — /api/discovery/* routes (strict loopback-only) Adds the discovery HTTP surface on top of the reporter DB module: GET /api/discovery/results list findings (optional ?providerId) GET /api/discovery/results/:id one finding (404 if absent) DELETE /api/discovery/results/:id delete a finding POST /api/discovery/scan scan a provider + persist findings POST /api/discovery/verify/:id mark a finding verified Authorization: strict loopback-only. "/api/discovery/" is added to LOCAL_ONLY_API_PREFIXES so the central authz pipeline (proxy.ts → runAuthzPipeline → managementPolicy) rejects non-loopback callers with a 403 LOCAL_ONLY before any handler runs. It is deliberately NOT in LOCAL_ONLY_MANAGE_SCOPE_BYPASS_PREFIXES — no remote manage-scope bypass — because POST /scan issues outbound probes to provider endpoints (SSRF-adjacent) and must never be tunnel-reachable. Handlers also call requireManagementAuth (defense in depth) and return sanitized errors via createErrorResponse. Tests: - tests/unit/authz/discovery-routes-local-only.test.ts (8) — security guard: isLocalOnlyPath true + not manage-scope-bypassable for all four paths. - tests/unit/api/discovery-routes.test.ts (6) — handler integration over an isolated DATA_DIR: list/filter, by-id 200/404/400, scan persist + 400 on empty/malformed body, verify 200/404, delete 200/404, no stack-trace leak. * feat(discovery): Phase 2c — dashboard UI tab (Tools → Discovery) Adds the /dashboard/discovery page (DiscoveryPageClient) that consumes the Phase 2b /api/discovery/* routes: scan a provider, list findings, verify or delete them. Registered in the sidebar under the Tools group (icon travel_explore) and given a "discovery" i18n namespace + sidebar keys in en.json (other locales fall back to en via next-intl until synced — the locale files are in a pre-existing coverage deficit unrelated to this change). Registers the UI test path in vitest.config.ts (advisory ui suite). Tests: src/app/(dashboard)/dashboard/discovery/__tests__/DiscoveryPageClient.test.tsx (3 cases: loads+renders results, empty state, fetches /api/discovery/results on mount; stable useTranslations mock to avoid the fetch-loop). NOTE: the ui vitest suite cannot run in this workspace — @testing-library/dom (a @testing-library/ react peer dep) is absent from node_modules, which fails ALL existing ui tests equally; the test runs in CI. Component verified locally via typecheck + lint. * test(discovery): register discovery-routes-local-only in stryker tap.testFiles The mutation-test-coverage gate (--strict) flags any unit test covering a mutated module that isn't listed in stryker.conf.json tap.testFiles. This PR's tests/unit/authz/discovery-routes-local-only.test.ts covers src/server/authz/ routeGuard.ts (a mutated module, which this PR edits by adding the /api/discovery/ local-only prefix), so it must be registered for its mutant kills to count. No behavior change. * refactor(discovery): split DiscoveryPageClient to satisfy max-lines-per-function The complexity ratchet (max-lines-per-function: 80) flagged the single 184-line DiscoveryPageClient function (+1 over baseline). Extract the data layer into two hooks (useDiscoveryResults for list/loading/feedback, useDiscoveryActions for scan/verify/delete), a shared callApi helper, and two presentational sub-components (DiscoveryScanForm, DiscoveryResultCard). Every function is now under the 80-line ceiling; complexity gate back to baseline 1995. No behavior change — same exported component, same endpoints, same props. * test(sidebar): include discovery in omni-proxy item-order snapshot Adding the Discovery item to the Tools group (this PR's sidebar entry) extends the ordered omni-proxy section list. Update the exact-match deepEqual snapshot in sidebar-visibility.test.ts to include "discovery" in its position (after traffic-inspector). The assertion stays exact — this reflects the intentional new item, it does not weaken the check. * docs(changelog): restore release bullets eaten by merge auto-resolve; re-add discovery bullet additively * chore(quality): bump testFrozen for translator-openai-responses-req.test.ts (1097 -> 1172) Base-red inherited from #5933, which grew the test file to 1171 lines (Hard Rule #18 regression tests) without adjusting the frozen cap. The release tip itself fails check:file-size; this unblocks every PR into release/v3.8.44. File untouched by this PR. * chore(quality): restore stryker tap.testFiles entries eaten by merge auto-resolve The merge of origin/release/v3.8.44 silently dropped the 3 entries added on the release side (#5903, clinepass, #5923). Took the release version verbatim and re-added only this PR's entry (discovery-routes-local-only) in alphabetical order. check:mutation-test-coverage green locally. * chore(quality): reconcile inherited v3.8.44 merge-burst drift + include discovery in tools-group order test - complexity 1995->2003 and cognitive 856->859: both measure IDENTICAL on the pristine release tip ( |