* 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.
* fix: repair five base-red failures on release/v3.8.49
Every PR cut from this branch fails CI on the branch's own breakage. Five
distinct causes, none introduced by the PRs that trip over them:
1. dast-smoke / Turbopack build — src/sse/handlers/chat.ts imported
PROVIDER_BREAKER_FAILURE_STATUSES twice in one statement. A duplicate import
specifier is an ECMAScript syntax error, so the production build never
compiled. Introduced by #8258, whose export fix landed on top of an import
that already existed.
2. Unit Tests — the #8393 verified-cooldown bypass was renamed
exactCooldownVerified -> exactCooldownIsUpstreamReset during the #8254
conflict resolution, which also dropped the flag at the markAccountUnavailable
call site entirely. The rename left the test passing the old key (so the flag
was silently ignored and a verified upstream reset got clamped back to
maxCooldownMs), and the dropped call site meant no real caller set it at all.
Align the test on the surviving name, restore the call site, and restore the
doc comment explaining #6863 vs #7940.
3. Unit Tests — #8526 added four common.* keys to en.json only, breaking the
strict key-parity tests for pt-BR and vi. Translated into all 42 locales.
4. Unit Tests — vi carried 17 __MISSING__ placeholders from #8354 and #8463, and
vi is the one locale with a no-placeholder test. Translated.
5. No new ESLint warnings — four suppressed `any`s in
tests/unit/combo-routing-engine.test.ts no longer exist, and ESLint exits 2 on
stale suppressions. Pruned (271 -> 267); no other entry moved.
Also regenerates skills/cli-backup-sync/SKILL.md, which still documented the
`backup status` flags #8512 removed — the merge-integrity gate compares the
generated output against the tree.
Not fixed here: the env/docs contract (NEXT_PUBLIC_OMNIROUTE_BASE_PATH and
OMNIROUTE_BACKUP_SCHEDULE_JOB_INTERVAL_MS missing from .env.example), which #8690
already covers, and the quality baselines, which #8686 covers.
* fix(quality): keep prettier off the generated SKILL.md files
check:agent-skills-sync diffs the generator's output against the tree byte for
byte, but lint-staged runs prettier over any staged *.md — and prettier inserts a
blank line after the frontmatter that the generator does not emit. Committing a
regenerated skill therefore made the gate fail again on the very file that was
just brought back in sync. The 44 untouched skills only escape this because they
never pass through lint-staged.
The generator is the formatter of record for these files, so ignore them.
* fix(i18n,quality): drop the stale zh-TW key; raise the auth.ts frozen cap
#8463 renamed `oauthModal.googleOAuthWarning` away but left the old key behind in
zh-TW, so the "the stale googleOAuthWarning key is GONE from every locale" guard
fails on the branch. Removed it.
The auth.ts frozen line cap goes 2486 -> 2492. Restoring the dropped
exactCooldownIsUpstreamReset call site costs 7 lines, and staging the file makes
lint-staged reformat four pre-existing over-100-column lines to prettier's rule —
unavoidable without bypassing the hook, which hard rule #10 forbids. The file
still sits 12 lines below where the cap was set relative to its actual size.
`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.
Both vars were introduced in code without a .env.example / ENVIRONMENT.md
entry, so `check:env-doc-sync` (docs-sync-strict / docs-gates) went red on
release/v3.8.49 with "In code but missing from .env.example: 2":
- NEXT_PUBLIC_OMNIROUTE_BASE_PATH — src/shared/hooks/useDisplayBaseUrl.ts (#8514)
- OMNIROUTE_BACKUP_SCHEDULE_JOB_INTERVAL_MS — src/lib/jobs/backupScheduleJob.ts (#8517)
Documents both in .env.example and docs/reference/ENVIRONMENT.md rather than
adding allowlist entries: both are real operator-tunable knobs, so the
allowlist would hide a genuine gap.
Refs #8540
Co-authored-by: rqzbeh <rqzbeh@users.noreply.github.com>
Co-authored-by: maxmad64bis <maxmad64bis@users.noreply.github.com>
* fix(sse): export PROVIDER_BREAKER_FAILURE_STATUSES so chat.ts stops throwing ReferenceError
Base-red slice 4 (single root cause across the whole handleChat cluster).
src/sse/handlers/chat.ts:1273 references PROVIDER_BREAKER_FAILURE_STATUSES to decide
whether an all-rate-limited provider result should trip the provider breaker, but the
constant was only a FILE-LOCAL const in chatPredicates.ts (a refactor extracted it out
of chat.ts and never re-exported it). Every request that reached that branch threw
`ReferenceError: PROVIDER_BREAKER_FAILURE_STATUSES is not defined`, so the global
fallback and breaker-gate paths blew up — surfacing as "All models failed |
PROVIDER_BREAKER_FAILURE_STATUSES is not defined" and breaking the handleChat coverage
tests (combo-error passthrough, 503 for cooled-down/open-breaker, budget-error, model
cooldown, body-derived retry-after, non-JSON rate-limit bodies).
Fix: export the const from chatPredicates.ts and import it in chat.ts (one canonical
definition, restoring the pre-refactor behavior).
Validated: chat-route-coverage 15/0 (was 12/3), chat-cooldown-aware-retry 6/0,
chat-rate-limit-body-lock 2/0; breaker guards (7907, combo-breaker-429, openrouter-6842)
unchanged; typecheck:core clean.
* test(nvidia): read PROVIDER_BREAKER_FAILURE_STATUSES from chatPredicates.ts
Same root cause as the chat.ts import fix in this PR: the const was extracted out
of chat.ts into chatPredicates.ts, so the nvidia-quota Phase-1 guard (which greps
the source for the `= new Set([...])` declaration to prove 429 was not added to the
breaker classification) must read chatPredicates.ts, not chat.ts. Now 13/0.
---------
Co-authored-by: Probe Test <probe@example.com>
* test(antigravity): align catalog tests with the #8013/#8123 model realignment
Base-red slice 3. Two test files referenced antigravity model IDs that
models) intentionally renamed/retired — the candidate builder and static catalog
are correct; the tests were stale.
- auto-combo-credentialed-model-pool: claude-sonnet-5 -> claude-sonnet-4-6 and the
gemini-3.5-flash-{low,medium,high} tier -> gemini-3.6-flash-{low,medium,high}
(verified against the live createVirtualAutoCombo candidate set); the exclusion
wildcard and per-account transparency assertions are unchanged in intent.
- T31 static-catalog: gemini-3-pro-preview was retired by #8013, so assert the
current client-visible top flash tier (gemini-3.6-flash-high) plus its absence.
Test-only. Validated: auto-combo-credentialed-model-pool 4/4, the T31/T33/T34/T38
model-specs file 8/8.
* test(model-alias-seed): expect canonical antigravity provider for the agy alias
Same #8013 realignment as this PR: `getModelInfo` resolves the stored `agy/…` alias
target to its canonical provider id `antigravity` (ALIAS_TO_PROVIDER_ID). The stored
alias STRING stays `agy/gemini-pro-agent`; only the resolved `provider` is now
`antigravity`, not `agy`. Test updated to match. 6/0.
---------
Co-authored-by: Probe Test <probe@example.com>
* fix(chatcore): use combo-resolved context limit in enforceOutputTokenBudget
Line 1801 called getTokenLimit() directly, ignoring the contextLimit
variable that was already resolved with combo overrides (e.g. user-set
201320 for nvidia/z-ai/glm-5.2). This caused enforceOutputTokenBudget
to use the fallback 128K default, capping max_tokens to near-zero and
silently truncating responses.
Fix: use the existing contextLimit variable instead of re-resolving.
* fix(chatcore): hoist combo-resolved contextLimit so the output-token budget honors it
contextLimit (including the combo override from resolveComboContextLimit())
was declared inside the proactive-compression `if` block and never survived
to the final enforceOutputTokenBudget() call further down in
handleChatCore(), which referenced an out-of-scope `contextLimit` — a
ReferenceError on every request. Hoist the declaration to function scope so
the combo-resolved context limit is what the output-token budget actually
enforces.
Adds a regression test that drives handleChatCore() end-to-end with a combo
whose resolved context limit differs from the plain per-target
getTokenLimit() lookup, since output-token-budget.test.ts only exercises
enforceOutputTokenBudget() directly and cannot catch this class of bug.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: TonPro <hello@tonpro.fu>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
* fix(sse): label HTTP 499 disconnects as client_disconnected
Preserve caller-supplied error type/code in buildErrorBody so stream
abort classification is not overwritten by the status-code table.
* docs(security): document buildErrorBody classification arg
---------
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
* fix(guardrails): Vision Bridge describe-fallback ignores unreachable candidates
getVisionCapableModels() scanned the entire static PROVIDER_MODELS catalog
for a describe-fallback model without checking whether the provider has a
usable active connection on this instance. On an instance with no `openai`
provider connected, this let the hardcoded default `openai/gpt-4o-mini` win
selection every time, the describe call would fail, and
replaceImageParts()'s describe-failure fallback (#4012) intentionally
preserves the raw image part — which then reaches a non-vision backend and
gets rejected with an opaque upstream error (e.g. "unknown variant
`image_url`, expected `text`").
Extract the credential-usability check already used by the whole-request
reroute path (visionBridge.ts's hasUsableCredentialsForModel /
isProviderConnectionUsable) into a shared visionBridgeCredentials.ts module,
and apply the same check to the describe-fallback candidate list in
visionBridgeRouter.ts. A confirmed-unusable connection (`false`) excludes a
candidate; an indeterminate result (`null`, e.g. no DB) fails open to
preserve existing behavior.
getVisionCapableModels/getBestVisionModel/getFallbackModels become async to
support the credential lookup; call sites and the existing unit test suite
are updated accordingly, plus new coverage for the exclusion behavior.
* fix(guardrails): fix flaky credential-mock race and move Vision Bridge router tests to a CI-blocking runner
The two new assertions added in this PR (excludes a candidate with no
usable active connection / selects a credentialed candidate over an
uncredentialed one) were correct — the failure was a genuine Vitest
race: getVisionCapableModels() fans out to hasUsableCredentialsForModel()
once per catalog entry via Promise.all, and dozens of concurrent
first-load `await import("@/lib/db/providers")` calls for the same
specifier under vi.mock() nondeterministically resolved against the real
module instead of the mock for some callers. Memoize the dynamic import
in visionBridgeCredentials.ts so it resolves exactly once and is reused,
which removes the race entirely (and is cheaper at runtime too).
Also: tests/unit/guardrails/visionBridgeRouter.test.tsx was never
collected by any CI-blocking gate — `test:unit`'s guardrails glob is
`*.test.ts` only, and `test:vitest` (vitest.mcp.config.ts) doesn't
include this directory; only the advisory `test:vitest:ui` picked it up.
Moved the suite to visionBridgeRouter.test.ts under node:test, threading
an optional `deps.hasUsableCredentials` injection point through
getBestVisionModel()/getFallbackModels() (consistent with the existing
deps pattern in visionBridge.ts) since this project's native test runner
has no supported ESM module-mocking mechanism.
Running the full guardrails suite together also surfaced that this PR's
own credential-exclusion feature silently broke the pre-existing
vision-bridge-callmodel.test.ts fallback-retry test: with zero seeded
provider connections in that test's isolated DATA_DIR, every fallback
candidate is now confirmed-unusable and excluded, leaving no fallback to
retry. Seeded one credentialed connection there so the fallback-retry
mechanics stay independent of the (unrelated) credential filter.
Refs #8433
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>
* fix(oauth): explain the LAN-IP loopback mismatch with an actionable panel
#8046 already stops the doomed login when a PKCE_CALLBACK_SERVER_PROVIDERS
provider (codex / xai-oauth / grok-cli) is connected from a LAN IP, but it
explained itself as one long English sentence rendered in the generic red
"Connection failed" step. Two concrete problems with that:
- the operator had to parse prose to work out WHICH ports to forward, and the
command shipped with `<port>` / `<omniroute-host>` placeholders to resolve
by hand;
- it forwarded a single port. Both are required: the dashboard port is what
makes the origin true-localhost (a LAN origin never reaches the
callback-server branch at all), and the provider's fixed callback port is
where the browser is actually sent back to. Forwarding either one alone
still fails.
buildPkceLoopbackMismatchHint() now returns the diagnosis as structured data
with the detected host and both ports already filled in, and a dedicated
OAuthLoopbackMismatchPanel renders it as: what happened -> how to fix, in
three numbered steps with copy-to-clipboard fields. No "Try again" button —
retrying the same origin fails identically. The panel yields to the
paste-token tab so grok-cli (which is in both provider sets) never stacks the
two views.
The flat warning string stays exported for non-UI callers.
docs: REMOTE-MODE.md gains a "Connecting Codex / Grok on a remote install"
section with the fixed-callback table and the two-port tunnel, mirroring the
existing Antigravity section.
i18n: 9 new oauthModal keys, hand-written for en + pt-BR and propagated to the
remaining 40 locales as `__MISSING__:` sentinels (runtime falls back to the
clean English value per #7258).
* fix(oauth): correct the Antigravity remote-login guidance and drop the stale i18n copy
Same LAN-origin family as the codex fix in this branch, different mechanism and a
worse failure mode.
Google providers (antigravity / agy) have no fixed foreign port: OAuthModal builds
`http://127.0.0.1:<dashboardPort>/callback`. On a LAN origin that 127.0.0.1 is the
BROWSER's machine, and Google's firstparty/nativeapp consent only releases the code
once the loopback is reachable from the approving browser. When it is not, the consent
never redirects at all — it hangs. So unlike an ordinary provider there is no error
page and no callback URL in the address bar.
That made the existing copy actively wrong. `googleOAuthWarning` was corrected when
the login helper shipped (#5203), but a changed English value does not invalidate
existing translations and `i18n:sync-ui` only fills keys that are ABSENT, never ones
that are STALE — so 39 of 43 locales (pt-BR, pt, es, de, fr, ja, zh-CN, …) kept the
original "wait for the redirect, copy the full URL and paste it below", instructing a
flow that cannot complete. The drift gate that should have caught this is a no-op:
`check-translation-drift.mjs` needs `.i18n-state.json`, which is not in the repo, and
it runs `--warn`.
Because the key's MEANING changed, it is renamed rather than edited — a new key cannot
inherit a stale translation. `googleOAuthWarning` is removed from all 43 locales and
replaced by 7 `googleLoopback*` keys, hand-written for en + pt-BR and marked
`__MISSING__:` elsewhere so the runtime falls back to correct English (#7258).
UI: `OAuthGoogleLoopbackNotice` states what is happening and surfaces both real
remedies with the detected host and port filled in — the local login helper
(recommended; its blob is what the Step 2 field accepts) and a single-port SSH forward.
It also REPLACES `remoteAccessInfo` for this family instead of stacking on top of it:
that notice promises an error page whose URL you copy, true for ordinary providers and
false here.
`agy` deliberately gets no helper command. bin/cli/commands/login.mjs pins
PROVIDER = "antigravity" and parsePastedCredentials() rejects a blob whose embedded
provider does not match the route provider, so advertising the helper there would send
the operator to a blob guaranteed to be refused. It keeps the tunnel path.
Refactor: the shared `resolveDashboardPort` / `buildSshLocalForward` helpers move to
`loopbackTunnel.ts`, used by both hint builders. The codex builder's behaviour is
unchanged (its 11 tests still pass untouched).
docs: REMOTE-MODE.md notes that the dashboard now surfaces the remedies, states that
one forward is enough for Antigravity (contrasting the two-port codex case), and aligns
Option B's command on 127.0.0.1 to match what the UI generates.
---------
Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
* fix(backend): fail closed when capability filters empty the combo pool
Tools/vision/structured_output filters no longer re-admit the full pool when
every target is incompatible. Opt-in via combo config compatFilterFailOpen.
Closes#8488
* fix(backend): keep tool-emulation providers under fail-closed filters
Carve out providers with toolCalling:"emulated" (#5240) from tools
capability_mismatch so chatgpt-web combos still reach the prompt shim.
Align round-robin compatFilterFailOpen with settings fallback and drop
new any-typed params from the #8488 combo-routing tests.
* chore(lint): prune stale combo-routing-engine any suppressions
Test cleanup in #8488 dropped four no-explicit-any hits; sync the freeze file.
* chore(quality): rebaseline combo.ts + freeze new-above-cap comboStructure.ts
check:file-size was red for this PR's own growth: combo.ts grew 3640->3693
(+53, the capability-filter fail-closed guard + compatFilterFailOpen escape
hatch at both call sites) and combo/comboStructure.ts crossed the 800-line
new-file cap at 918 (describeCapabilityFilterExhaustion +
providerSupportsEmulatedToolCalling for the #5240 emulated-tool-calling
exemption). Both are irreducible orchestration wiring at the existing combo
filter chokepoint (same precedent as #7301's cooldown-retry generalization).
Companion test tests/unit/combo-routing-engine.test.ts frozen at its own
grown size (3409->3449). No logic change; 95/95 tests pass.
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
---------
Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
Co-authored-by: Prudhvivuda <Prudhvivuda@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* feat(log): Added new visual scrolling log page
* chore(quality): rebaseline sections.ts own-growth for #8354 (logs-timeline sidebar item)
* feat(log): direct-link a request from the scrolling timeline
Clicking a request bar now sets ?id= on the URL (matching the regular
request log page), and the timeline opens the deep-linked request on
mount. The open/close handlers arm the same guard so a stale
initialSelectedId (router.replace() commits the URL after the render it
triggers) can never reopen the modal right after the user closes it.
* fix(dashboard): propagate logsTimelineSubtitle across all 43 locales
en.json was missing the logsTimelineSubtitle key entirely, breaking the
default locale for the new /dashboard/logs/timeline sidebar entry. Add
the real English string to en.json, add __MISSING__: placeholders to
the 11 locales that lacked the key outright, and convert the 30 locales
that had copied the English text literally to the repo's __MISSING__:
convention for untranslated strings.
Also pause the RequestTimeline 2s poll while the tab is backgrounded
(document.visibilityState), matching the existing pattern in
RequestLoggerV2 and UsageStats.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(i18n,sidebar): add missing logsTimelineSubtitle + update sidebar tests
Address maintainer feedback on #8354:
- Add logsTimelineSubtitle key to en.json + 11 locales (ar, az, bg, bn,
cs, da, de, es, fa, fi, fr) that were missing it
- Add logs-timeline to sidebar-visibility.test.ts expected arrays
- Add logs-timeline to sidebar-monitoring-reorg.test.ts logs group
- Add compression-exclusions to sidebar-visibility.test.ts (pre-existing)
* feat: add touch support for mobile panning and pinch-to-zoom on timeline
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(dashboard): include OMNIROUTE_BASE_PATH in displayed API base URL
When OmniRoute is served under a reverse-proxy subpath (OMNIROUTE_BASE_PATH),
the Endpoints UI built display URLs from window.location.origin alone and
appended /v1, producing https://host/v1 instead of https://host/omniroute/v1.
- Prefer NEXT_PUBLIC_BASE_URL when it already includes a non-root path
- Append NEXT_PUBLIC_OMNIROUTE_BASE_PATH (mirrored from OMNIROUTE_BASE_PATH
at build time) when resolving a bare public origin
- Document subpath display behavior in .env.example
- Add unit coverage for basePath-aware resolution
* docs(changelog): add fragment for #8514 display basePath fix
---------
Co-authored-by: rqzbeh <rqzbeh@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
* feat(combos): add select all / unselect all in Browse Catalog
* fix(dashboard): guard combo Select all + test the real batch handlers (#8526)
Select all had no cap — with "Show configured only" off, or a large
provider catalog, one click could add hundreds of models to a combo.
ModelSelectModal now confirms above SELECT_ALL_CONFIRM_THRESHOLD (20)
before batch-adding, matching the native confirm() pattern already
used for bulk/destructive actions elsewhere in the dashboard.
Also extracts ComboFormModal's handleAddModels/handleDeselectModels
batching logic into computeBatchAddModelSteps/computeBatchDeselectModelSteps
(src/lib/combos/builderDraft.ts) so unit tests exercise the real
implementation instead of a hand-maintained mirror that could drift
from the component and stay green while production code broke.
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>
* fix(chatgpt-web): recover async images via conversation-poll fallback (#7357)
chatgpt-web generates images upstream but frequently fails to return them:
register-websocket is Cloudflare-sensitive and the plain WebSocket used to
receive the async image event lacks the browser TLS fingerprint the HTTP
client (tlsFetchChatGpt) uses, so pollForAsyncImage errors or times out with
no frames — even though the image is already in the conversation.
When the websocket yields nothing, poll GET /backend-api/conversation/{id}
over the same authenticated HTTP path and read the image_asset_pointer
directly (newest message wins, so a reused conversation can't surface a stale
image). The existing makeImageResolver then downloads it via the files API.
This is the durable fallback suggested in #7357.
Verified against a live ChatGPT Plus session: with the websocket capped short,
the fallback recovers the image and returns a real PNG.
* test(chatgpt-web): cover conversation-poll fallback for async images
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: sadruzzahan <istykhan.ik@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
* feat(cli-tools): add all Hermes Agent auxiliary model roles
Extend HERMES_AGENT_ROLES from 7 to 18 slots to match the full
auxiliary.* set in Hermes Agent config.yaml:
- add: mcp, title_generation, memory_query_rewrite, tts_audio_tags,
triage_specifier, kanban_decomposer, profile_describer, goal_judge,
curator, monitor, background_review
- reorder: web_extract before compression (match upstream docs)
Backend generator/reader are generic on auxiliary.<role> — only the
role catalog, UI card, and i18n (en + pl) needed updating.
* fix(i18n): translate the 12 new Hermes auxiliary roles into vi and pt-BR
The 22 new en.json keys landed only in pl.json, breaking the vi and pt-BR
key-parity guards. Adds the same keys with real translations (no placeholder
strings), keeping both parity assertions exact.
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
* test(cli-helper): guard HERMES role catalog parity between backend and UI
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* test(tail): retire stale i18n __MISSING__ repro + fix qianfan website URL
Base-red slice 6, rebased onto the advanced release/v3.8.49 (91fd5f9). The oauth
grok-cli #7610 guard was already fixed on the base by #8027 (it reads the warning
from grokCliAuthJson.ts) — dropped from this slice to avoid a conflicting duplicate.
Remaining two, still red on the current base:
- i18n #7258: the "focused repro" asserted zh-TW.json STILL carries raw __MISSING__:
placeholders. That backlog was filled (the "no locale has a raw __MISSING__: leaf"
invariant is the durable guard); retired the now-inverted repro.
- qianfan: Baidu renamed the product page (product/wenxinworkshop -> product-s/
qianfan_home); updated the expected website URL.
Validated (clean env): i18n 4/0, qianfan 5/0; oauth-modal-grok 2/0 already green on base.
* fix(resilience): short-circuit combo on input-bound failures (context_length_exceeded) (#8375)
isInputBoundRequestFailure() predicate detects deterministic input-bound
errors (context_length_exceeded/context_window_exceeded). The combo loop
propagates the original 400 immediately instead of burning MAX_GLOBAL_ATTEMPTS
retrying identical oversized inputs against every account.
Test: combo-input-bound-failure-8375.test.ts (1 test, 2 assertions)
* fix(resilience): add early-exit in combo dispatcher for input-bound failures (#8375)
When isInputBoundRequestFailure detects context_length_exceeded,
the combo loop returns {ok:false, response} immediately instead of
re-dispatching the oversized request.
Test: node --import tsx/esm --test tests/unit/combo-input-bound-failure-8375.test.ts
- 1 test, 2 assertions, 0 fail
* fix(translator): strip input_image from tool outputs in Responses->Chat downgrade (#8459)
toolOutputContentToString() extracts input_text/output_text parts and
replaces input_image with a placeholder instead of JSON.stringify'ing
the content-part array (which embedded raw ~52KB base64 as inert text).
Applied to both function_call_output and custom_tool_call_output branches.
Existing translator tests: 88/88 pass.
New tests: 4/4 pass.
* fix(providers): set qwen-web toolCalling to false — web-cookie provider has no native function calling (#8437)
qwen-web is a web-cookie provider that emulates tools via synthetic system
prompt text and <tool> XML parsing, never sending a native tools[] field
upstream. The filterTargetsByRequestCompatibility gate filters out non-tool-
calling targets when the request carries tools, but qwen-web's registry entry
had toolCalling=true, so the filter let it through and a tool-using session
failing over to qwen-web would silently degrade to text-only chat with
'Tool X does not exists' errors.
Sibling web-cookie providers (chatgpt-web, yuanbao-web, claude-web, etc.)
all correctly set toolCalling: false — qwen-web was an outlier introduced
in PR #7874.
Verification:
- LSP diagnostics: clean
- Pattern matches chatgpt-web, yuanbao-web, and other web-cookie providers
* fix(backend): empty upstream response mislabeled as exhausted_connection (#8397)
isEmptyContentFailure guard only matched '/empty content/i' but the actual
error text from detectMalformedNonStream is 'returned an empty response
(no usable choices/output)' — which lacks the word 'content'. Expanded
regex to also match '/empty response/i' so these transient upstream glitches
don't get classified as connection-level exhaustion in combo diagnostics.
Test: 28 existing combo-target-exhaustion tests pass (no new test needed)
* test(#8397): add regression test for empty-response 502 not marking provider/connection exhausted
* test(qwen-web): align registry snapshot with toolCalling:false
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(resilience): scope #8375 input-bound short-circuit to homogeneous remainders
The isInputBoundFailure short-circuit (context_length_exceeded /
context_window_exceeded) fired unconditionally on the first target, aborting
the whole combo even when later targets are a different model with a larger
context window — regressing the intentional heterogeneous-combo fallback that
isContextOverflow400 (#6637) protects. Reproduced with a 2-target combo
(small-context model fails, larger-context model would have succeeded): the
combo never reached target 2.
Scope the short-circuit to remainders where every remaining target shares the
same modelStr as the one that just failed — the "retrying will fail
identically" premise for context_length_exceeded only holds within a
homogeneous same-model pool.
Rebaselines open-sse/services/combo.ts's frozen file-size cap (3642->3679)
for this PR's own combo.ts growth (config/quality/file-size-baseline.json).
Refs #8375
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
---------
Co-authored-by: Probe Test <probe@example.com>
Co-authored-by: herjarsa <herjarsa@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
* feat: read INITIAL_PASSWORD env var during setup
Allow users to set the admin password via the INITIAL_PASSWORD
environment variable instead of requiring the --password CLI flag
or interactive prompt. Falls between --password flag and interactive
prompt in resolution priority.
* test(cli): cover INITIAL_PASSWORD env var in setup resolvePassword
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: linh.doan <linh.doan@be.com.vn>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(providers): resolve native vision for path-shaped multimodal model ids
Leaf-id static/registry metadata now wins over synced attachment=false without
modalities, so cp/cline-pass/kimi-k3 forwards images natively.
Closes#8032
* fix(providers): scope path-shaped leaf lookup to vision only
Move leaf MODEL_SPECS fallback out of shared getStaticSpec() so
aihorde/deepseek/deepseek-v4-flash no longer inherits DeepSeek tool-calling
metadata (#8212). Vision still resolves via getVisionStaticSpec().
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
---------
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
* fix(providers): honor Extra API Keys rotation in OpencodeExecutor
OpencodeExecutor.buildHeaders bypassed resolveEffectiveKey, so extraApiKeys
never rotated and an empty primary with populated extras sent no auth header.
Closes#8467
* fix(executors): gate the Authorization header write on the resolved effective key
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Sticky root keys keyed only on the first user message caused Claude Code
"New session" + "hi" to reuse a confirmed prior Notion thread (forking history).
Prefer exact conversation-prefix match for multi-turn; keep sticky root for
UREW multi-turn and failed-first-request retries; mint createThread:true when
the sticky root is already confirmed and the request has no assistant history.
* feat(adobe-firefly): reference image attach + /v1/images/edits (follow-up #8006)
Upload source images to Firefly storage (POST /v2/storage/image) and attach
them as referenceBlobs on generate-async, matching live firefly.adobe.com
captures (usage:general for nano multi-ref; usage:subject for gpt-image).
Also wire built-in adobe-firefly through OpenAI-compatible POST /v1/images/edits
(multipart or JSON data URLs, up to 4 refs) so Media edit-with-references
and Open WebUI image-edit hit the same path as image2image generate.
Unit suite: tests/unit/adobe-firefly.test.ts 41/41.
* test(api): add route-level coverage for Adobe Firefly /v1/images/edits + fix typecheck/file-size drift
Covers the referenceBlobs upload path, the 4-reference cap error, and the
credentials/rate-limit branches added to the /v1/images/edits route for
adobe-firefly (#8510). Also fixes a Buffer/BodyInit typecheck mismatch in
uploadAdobeFireflyImage and corrects the adobeFireflyClient.ts file-size
baseline entry to match the gate's actual LOC count (it counts the trailing
newline, so the frozen value is 2317, not 2316), plus a testFrozen entry for
adobe-firefly.test.ts's own +159 line growth from this PR. Moves the
handleAdobeFireflyImageGeneration re-export out of the middle of the import
block in imageGeneration.ts for readability.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(security): restore bounded JWT regex quantifiers dropped by edit-route refactor
The edits-route extraction (test commit 62785e972f) had accidentally
reverted 4 JWT-extraction regexes in adobeFireflyClient.ts from the
bounded {1,4096} quantifier back to unbounded +, undoing the ReDoS fix
from #8173 (CodeQL #754/#755/#756). Restored the bounded quantifiers;
behavior is unchanged (45/45 adobe-firefly tests still pass), only the
backtracking bound is back in place.
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
---------
Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
* fix(hyperagent): default 1M context for fable/opus/sonnet
HyperAgent Claude-family models (fable, opus, sonnet) were falling through
getTokenLimit to the generic 128k default. Agentic tool-loop prompts with
large catalogs then failed with context_length_exceeded (~137k tokens).
- defaultContextLength + per-model contextLength = 1_000_000 on hyperagent registry
- DEFAULT_LIMITS.hyperagent / ha = 1M
- Resolve hyperagent/ha (and fable/opus wire ids) before models.dev DB fallback
Verified: getTokenLimit('hyperagent','fable-latest') === 1000000; context-manager tests 30/30.
* fix(sse): scope hyperagent 1M context fix to the registry, drop unscoped model-name match
The step-1b branch in resolveTokenLimit() matched fable/opus/sonnet model
name substrings for ANY provider, before the models.dev DB lookup. That
collided with anthropic/claude, kiro, windsurf and bluesminds registries,
which serve the same Claude model ids (e.g. claude-opus-4.7-max,
claude-sonnet-5) with their own accurate per-model contextLength — those
were being clobbered to 1M instead of their real (often 200k) limit.
The registry-level defaultContextLength added on the hyperagent provider
entry already fixes the reported bug (getTokenLimit('hyperagent', ...) ===
1_000_000) on its own, scoped correctly by provider. Remove the redundant,
unscoped substring branch and add regression coverage for every hyperagent
fallback model id plus the cross-provider collision.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
isUsableChatModel() was copy-pasted into 5 vscode listing routes. PR #7012
widened only models/route.ts to accept api_format "responses"/"openai-responses"
alongside "chat-completions"; the other 4 copies (token+raw api/tags and
api/show) still rejected anything that wasn't literally "chat-completions",
silently dropping Codex-discovery-synced GPT models (apiFormat "responses")
from the Ollama-compatible /api/tags endpoint VS Code's "Ollama" provider
import flow actually calls.
Extracted the predicate into a single shared module
(vscode/[token]/usableChatModel.ts) imported by all 5 routes so this
one-fixed-four-left-behind drift cannot recur.
Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
POST /api/combos and PUT /api/combos/[id] had zero validation or
observability when a combo name collided with a real model id, and
sseModelService.getComboForModel() always resolves the combo first. That
combo-first precedence is not a bug: #6940 documents a combo named after a
bare model id (e.g. `gpt-5.5`) as the supported mechanism for per-model
provider fallback, reusing the #3227/#3233 machinery and covered by
tests/unit/responses-combo-resolution-3227.test.ts and
tests/unit/combo-name-codex-responses-rewrite.test.ts. Hard-rejecting a
colliding name (as #8530's literal acceptance criteria requested) would
regress that documented workflow.
Instead, both routes now attach a non-blocking `warning` field
(`COMBO_NAME_SHADOWS_MODEL`) to the create/rename response when the name
collides with a real model id, and a new boot-time scan
(scanComboModelNameCollisionsAtBoot in src/instrumentation-node.ts) logs a
startup warning enumerating existing collisions — so an operator who hits
this by accident has a signal, while the #6940-sanctioned pattern keeps
working exactly as before.
New tests/unit/combo-model-name-collision-8530.test.ts proves both: the
sanctioned shapes (create/rename to a colliding name) still return
201/200 with the warning attached, and non-colliding names get no warning
field at all.
Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
The zh-TW catalog was machine-translated with mainland-habit vocabulary and
simplified->traditional conversions that picked the wrong homophone, and its
README still advertised the v3.7-era figures.
Terminology (docs/i18n/zh-TW/ + src/i18n/messages/zh-TW.json):
- wrong-character conversions: 上遊->上游, 後臺->後台, 儀錶板->儀表板
- mainland habits: 默認->預設, 緩存->快取, 模塊->模組, 調用->呼叫,
字符串->字串, 全局->全域, 文檔->文件, 響應->回應
- consistency: 供應商/提供商->提供者 (提供者 was already 71% dominant),
型別->類型 for UI labels, 不活躍->未啟用
README figures synced to the English source: 231->290 providers,
17->19 routing strategies, 1.6B->1.53B free tokens, 50+->90+ free tiers,
11->40+ free forever, 87->104 MCP tools, 30->31 scopes.
Root cause — the generator's post-translation pass was a hardcoded list that
duplicated the glossary and was wired only into the deprecated
generate-multilang.mjs, so the active run-translation.mjs pipeline applied
nothing. Worse, its blanket /代碼/g -> 程式碼 rule would corrupt 控制代碼
(handle), 語系代碼 (locale code) and 錯誤代碼 (error code) on the next
regeneration.
Both scripts and the drift gate now share scripts/i18n/glossary-normalize.mjs,
driven by scripts/i18n/glossary/<locale>.json as the single source of truth.
Ambiguous terms carry blockedPrefixes so 型別->類型 can stay enforced without
mangling 模型別名 (model alias) or 基本型別 (a programming data type); terms
whose synonym is also a legitimate rendering elsewhere (代碼, 項目) are seeded
with no synonyms and documented instead of blanket-rewritten.
CI now runs the glossary gate for zh-TW alongside zh-CN.
buildClientRawRequest deep-cloned the ENTIRE request body on every chat request, unbounded.
On the #7847 incident payload (3.05 MiB, 729 messages, 86 tools) that retains 3.19 MiB per
request, and it is pure waste: every consumer of clientRawRequest.body is observability and
none of them keeps the full payload.
chatCore.ts -> reqLogger.logClientRawRequest no-op when the logger is disabled, otherwise
re-clones via cloneBoundedForLog (0.08 MiB)
chatCore.ts -> trackPendingRequest clientRequest, surfaced by /api/logs/[id]
chat.ts -> recordRejectedRequestUsage requestBody
None feeds dispatch, translation or the upstream request, so the snapshot is now taken with
cloneBoundedForLog: 3.19 MiB -> 0.08 MiB, a 41x reduction, and retention no longer scales with
history length. It stays a clone rather than an alias because body is rewritten downstream
(plugin onRequest hook, compression) and the log must show what the client actually sent.
Bounding at the entry means the logger re-bounds an already-bounded value, which exposed that
cloneBoundedForLog was NOT idempotent -- each container exceeded its own bound once the marker
was added, so a second pass truncated again:
arrays [marker, ...24 items] is 25 entries > 24, so the marker and one real item were
dropped and originalLength was rewritten as 25 instead of the true 729
objects 80 keys + _omniroute_truncated_keys is 81 > 80, so a real key was evicted to make
room for the marker and the dropped count was reported as 1 instead of 20
strings the marker was appended AFTER slicing to maxLength, so the bounded string was
longer than the bound
Without this the persisted log payload would have changed shape versus before the fix. All
three now keep the marker inside the budget and treat an already-bounded value as final;
verified end to end -- the marker still reports originalLength 729.
TDD: tests/unit/repro-7847-bound-client-raw-request.test.ts was written first and failed on
three assertions (unbounded retention, retention scaling with history, and the idempotence
precondition) before either change.
#7847 reports a 3.05 MiB request (729 messages / 86 tools) reaching ~12,282 MiB of V8
heap, and asks for "a regression benchmark that records peak heap for representative
500-800-message, tool-rich requests" before any fix lands. There is currently no memory
baseline in the repo at all (bench:compression is the only benchmark), so a clone-reduction
change could neither be justified nor regression-guarded.
npm run bench:heap-body attributes retained heap to each copy the chat path makes:
| mechanism | call site | retained | x wire |
| cloneLogPayload (unbounded) | chat.ts buildClientRawRequest | 3.18 MiB | 1.04x |
| cloneBoundedForLog (bounded) | requestLogger.logClientRawRequest | 0.04 MiB | 0.01x |
| structuredClone x3 (combo targets) | combo.ts attemptBody | 9.53 MiB | 3.12x |
| JSON.stringify (token estimate) | combo.ts estimateTokens | 3.06 MiB | 1.00x |
| per request (sum) | |15.81 MiB | 5.17x |
It measures the real production helpers rather than reimplementations, so a change to the
log bounds or the clone strategy is reflected directly.
Design notes:
- Deterministic: fixed-seed LCG, no Math.random(). Verified byte-identical across three
consecutive runs — without that, a before/after delta measures noise, not the change.
- Corpus lives in its own side-effect-free module so the unit test can import it without
booting SQLite (requestLogger transitively opens the DB at import time).
- Hermetic: DATA_DIR is redirected to a temp dir before importing, so the benchmark never
touches the operator's real ~/.omniroute store.
- Node, not bun: --expose-gc and V8 heap accounting are the measurement; another engine's
heap number would not describe the production runtime.
- --max-retained-mib exits non-zero, so this can become a CI gate once a target is agreed.
Reports only; wires nothing into CI and changes no production code.
Two changes with one root cause: several hot paths built a full JSON string only to read
its .length, and one of them silently changed the answer.
1. CORRECTNESS -- combo's fallback-compression trigger
estimateTokens(JSON.stringify(attemptBody)) took the STRING branch of estimateTokens,
which is ceil(length / CHARS_PER_TOKEN) over the raw JSON. An inline base64 image is
then charged as if every character of the data URL were prose. Measured on a 200 KB
inline image:
via string (before) 50,039 tokens
via object (after) 1,231 tokens
a 40x over-count, tripping fallback compression on requests nowhere near the context
window. This is the same class #8368/#8401 fixed on the request path; the combo call
site was missed. Passing the object routes through extractImageTokens, which charges
images structurally. Text-only bodies are unaffected -- verified identical, and pinned
by a test.
2. ALLOCATION -- jsonLength()
Adds an exact serialized-length walker: same O(n) scan, no string. Used by
estimateTokens' object branch and by streamReadinessPolicy (which runs on every
streaming request and only ever used .length).
Exactness matters because every consumer feeds a threshold, so this is property-tested
against JSON.stringify over 4000 generated structures covering escaping, lone
surrogates, omitted values, non-finite numbers, toJSON, Date, Map, cycles and BigInt.
Anything outside the plain-JSON subset falls back to JSON.stringify for THAT SUBTREE
only, so an exotic leaf never forces the message history back onto the allocating path.
Honest scoping of the memory win: the string was always transient, and V8 collects it
efficiently, so this is not 3 MiB of retained heap. Measured allocation churn over 20 calls
on a 3.06 MiB body: 3.1 MiB -> 0.5 MiB, about 6x less. The #8549 benchmark row for this
mechanism measures a HELD string and therefore overstates it; the correctness fix above is
the larger deliverable here.
* 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.
* chore(token-refresh): extract rotation/cas/circuit-breaker refresh logic into tokenRefresh/* leaves
* test(oauth): follow isUnrecoverableRefreshError to tokenRefresh/shared.ts
cad2c7285 moved isUnrecoverableRefreshError out of tokenRefresh.ts into
tokenRefresh/shared.ts. This suite asserts on source *text* (it regex-matches
the function body to prove the unrecoverable sentinel is returned), so the
move made it fail to find the definition — the only red test across the 23
tokenRefresh-related suites.
Repoint the read() at the file that now defines the body. The public surface
is unchanged: tokenRefresh.ts still re-exports the symbol, verified by import.
* docs(changelog): add fragment for this PR
* docs(auth): correct the #7338 attribution wording in the tokenRefresh header
The header claimed credit for KooshaPari's #7338 was "preserved via co-authorship on the
extraction commits", but none of the commits carries a Co-authored-by trailer -- and adding
one would be inaccurate, since this is an independent implementation against the current
tip rather than a reuse of that diff. The by-name credit for proposing the split stays;
only the false claim about the mechanism is removed.
* chore(auth): condense #8407 note in tokenHealthCheck to fit frozen file-size baseline
The 3-line comment added by #8426 pushed tokenHealthCheck.ts to 843 LOC,
2 over its frozen baseline of 841 (base-red on release/v3.8.49). Fold the
devin-cli rationale into one line — same meaning, gate green again.
* refactor(dashboard): extract provider card highlight into HighlightableProviderCard
#8349 left the back-navigation highlight concern inline in providers/page.tsx
(state + two callbacks + onCardClick/ref props repeated at 18 card call
sites), pushing the page to 1990 LOC over its frozen baseline of 1927.
Move the concern into a HighlightableProviderCard wrapper: each card instance
keeps its own copy of the highlighted id and only the card whose provider id
matches scrolls into view and highlights — same semantics as the single
page-level state, since resolveHighlightedCard already gates on the id match.
page.tsx drops to 1917 LOC (under the frozen baseline, no rebaseline needed).
* test(dashboard): cover HighlightableProviderCard wiring
Direct coverage for the wrapper extracted in the previous refactor:
mount-time scroll+highlight only when history.state.providerId matches,
no reaction on mismatch/null state, isolation across multiple card
instances, and click-time navigation recording via history.replaceState.
Note: the jsdom scrollIntoView shim is a plain no-op rather than a
vi.fn() — vi.restoreAllMocks() would otherwise restore the shared mock
and let the next test's spy inherit its accumulated call count.
* fix(executors): fix ACP protocol wire format for devin cli
* test(sse): replace no-explicit-any with AcpFrame type in devin-cli ACP test
no-explicit-any is a hard error in tests/; type the parsed JSON-RPC frames
with a local AcpFrame shape instead of (f: any) callbacks, and apply
prettier formatting to the same file.
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Eight TS2339s in stream.ts read properties off `never`. `never` here did not
mean unreachable code — it meant a type predicate was lying.
function isClaudeEventPayload(payload: unknown): payload is JsonRecord
...
const flushedParsed = bufferedPayload as JsonRecord;
const isClaude = isClaudeEventPayload(flushedParsed);
} else if (!isClaude) { // JsonRecord minus JsonRecord = never
The predicate answers "does this payload carry a Claude event type?" — a
question about contents, not type. A Claude event and a non-Claude one are both
JsonRecord, so `payload is JsonRecord` narrows nothing; applied to an argument
already typed JsonRecord it collapsed the negative branch to `never`, taking
`.id` and `.choices` with it.
Changed the return type to `boolean`. No call site depended on the narrowing:
both other callers pass the value to updateClaudeEmptyResponseLifecycle(), whose
parameter is `unknown`, and passthroughTailProcessor.ts already declares this
function as `(payload: unknown) => boolean` — so this aligns the definition with
how the codebase already consumes it.
280 -> 272, zero new, on a line-number-agnostic diff of the full tsc error set.
The diff is one line, and a type predicate has no runtime representation, so the
emitted JavaScript is unchanged.
No test added, and no gap to fill: the `never`-typed branch is real, reachable
and already covered from both sides — stream-numeric-ids.test.ts exercises
"normalizes numeric id in final chunk without trailing newline" (the flush path
with a non-Claude payload, i.e. the branch TS believed impossible) and "Claude
passthrough does not normalize numeric ids" (the other arm). 338/338 across the
48 SSE/passthrough suites.
No explanatory comment either: stream.ts sits exactly at its frozen file-size
cap (2887), so a single added line fails check:file-size. The reasoning lives
here and in the PR rather than costing a baseline bump on a frozen file.
Co-authored-by: backryun <busan011@ormbiz.co.kr>
* refactor(sse): declare the multipart/gRPC frame bodies as ArrayBuffer-backed
Seven TS2769 across three files, one root cause: a bare `Uint8Array` widens to
`Uint8Array<ArrayBufferLike>`, which admits `SharedArrayBuffer` and so is not
assignable to `BodyInit`. Every one of the seven is a `fetch({ body })` call
rejecting an otherwise-valid payload.
They flow from exactly two producers:
buildMultipartBody() audioTranscription.ts -> 5 sites here + 1 in audioTranslation.ts
grpcWebFrame() windsurf.ts -> 1 site
Both allocate with `new Uint8Array(length)`, which is always ArrayBuffer-backed,
so declaring `Uint8Array<ArrayBuffer>` states what the code already guarantees.
This is a narrowing of the declaration to match reality, not a cast at the call
sites — no `as BodyInit` anywhere, and the seven call sites are untouched.
280 -> 273, zero new, on a line-number-agnostic diff of the full tsc error set.
The runtime diff is two return-type annotations; nothing executable changed.
Tests: buildMultipartBody already has four direct tests, but they assert the
payload's *contents* (boundary, filename sanitization, MIME fallback) — none
assert the backing, which is precisely what the new type guarantees and what a
plausible switch to a pooled `Buffer.concat` would break. Added
multipart-body-arraybuffer-backing.test.ts (3 tests): the buffer is a plain
ArrayBuffer, the view spans it entirely at offset 0 (no pool remainder), and
both hold for a 64 KiB payload past Node's Buffer-pool threshold. 258/258 across
the 17 affected audio/windsurf suites.
Not included: the 3 remaining TS2339 in audioTranscription.ts (`data?.data?.…`
on an untyped poll result). Different root cause — grouped by kind, not by file,
per #8484.
* chore(quality): trim the buildMultipartBody doc so audioTranscription.ts stays under the 800 cap
My doc comment on buildMultipartBody added 9 lines to a file with 8 to spare
(792 -> 801 as check:file-size counts, cap 800), so this PR was failing the gate
on growth I introduced. Condensed the comment to 4 lines; the file lands at 796.
Rebuilt on top of the /green-prs sync merge (9973bf3bf) rather than force-pushing
my own rebase over it — the release-tip sync there is @ikelvingo's work and had
already been validated against this branch.
Delta unchanged: 280 -> 273, 7 fixed, zero new. 68/68 on the audio/windsurf
suites; typecheck:core and eslint clean; check:file-size now OK.
---------
Co-authored-by: backryun <busan011@ormbiz.co.kr>
Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
All 10 diagnostics in this file are the `strictNullChecks: false` narrowing
limitation: a boolean-literal discriminant narrows the positive branch but
leaves the negative one as the full union, so `if (!resolved.ok)` and the code
after `if (outcome.success)` could not see `status`/`error`.
Three unions, all module-private and untouched by any test, so per the rule
recorded on #8499 these are retagged rather than fixed with predicates —
predicates are for exported unions whose shape callers depend on:
resolveDesignerWebRequest ok: true|false -> state: "resolved"|"invalid"
DesignerWebStepResult done + success -> state: "pending"|"ready"|"failed"
The step union collapsed two booleans into one discriminant; `done`/`success`
encoded three states across two flags, which is also why the pending arm had no
`success` property for `outcome.success` to read.
Also narrowed runDesignerWebPollLoop's declared return from
`DesignerWebStepResult | {…504…}` to a new DesignerWebOutcome (ready | failed).
The loop returns a step only after confirming it is terminal, and otherwise
synthesizes a 504 — it can never return a pending step, and the old signature
claiming it could is what made `.success` unreadable on the union at all.
280 -> 270, zero new, on a line-number-agnostic diff of the full tsc error set.
Tests: unlike the previous slices this rewrote real control flow (three
conditionals), so the existing suite is doing actual work here —
microsoft-designer-web-6672.test.ts drives the handler end-to-end through 400,
401, immediate-ready, poll-then-ready, non-OK upstream and 504-timeout, i.e.
every arm but one. The "empty" arm (unrecognized 200 body -> terminal 502) was
tested only at the parser level, never through the handler, so the 502 itself
was unasserted. Added designer-web-empty-response-502.test.ts (2 tests) pinning
that it is terminal (exactly one fetch, no polling) and distinct from the 504
deadline path. Both suites pass against the parent commit too — the tests are
black-box through the exported handler, so they are agnostic to the discriminant
rename and prove the retag is behavior-preserving. 61/61 across the 3 suites.
Co-authored-by: backryun <busan011@ormbiz.co.kr>
`transformResponseFromProvider(providerConfig, data, options = {})` annotated
nothing, so TS inferred `options` as `{}` from its default value and rejected
every read of it: `top_n` x6, `documents` x4, `return_documents` x2 across the
DeepInfra and Voyage adapters. All 12 of the file's diagnostics, one cause.
Declared `RerankResponseOptions` from the JSDoc that already documents these
fields on handleRerank, with `documents` as `Array<string | { text?: string }>`
— the two forms the Cohere-compatible API accepts and the adapters already
branch on. `providerConfig` and `data` stay unannotated; only `options` was
producing errors and only `options` is touched.
280 -> 268, zero new, on a line-number-agnostic diff of the full tsc error set.
Tests: `{ text }` documents were only ever exercised through the *request*
adapter (#5332, #7809) — every response-adapter test passed plain strings, so
the `typeof doc === "string" ? doc : doc?.text` branch on the response side was
uncovered, and that branch is exactly what gives the declared type its union.
Added rerank-object-documents-response-path.test.ts (5 tests): object-form
documents resolved through both adapters, a mixed string/object array, the
`{}`-without-text fallback, and Voyage's index remap across an empty `{ text: "" }`
document. They pass against the parent commit's rerank.ts too — no behavior
change. 43/43 across the 6 rerank/media-cost suites.
Co-authored-by: backryun <busan011@ormbiz.co.kr>
`BaseGuardrail.preCall`/`postCall` return `GuardrailResult<unknown> | void`,
and that `| void` is deliberate: docs/security/GUARDRAILS.md documents returning
nothing as one of the three "no change" signals, and CredentialMaskerGuardrail
still declares it. So the union stays.
The problem is the consumption site. `registry.ts` reads `result?.block`,
`result?.message`, `result?.meta`, `result?.modifiedPayload` — but optional
chaining does not make `void` inspectable, and neither does a truthiness test.
That is all 12 TS2339s in the file: one union, six property reads, two dispatch
loops.
Fixed by funnelling the return through `unknown` once, in a local
`asGuardrailResult()` helper, so the loops work against a plain
`GuardrailResult | undefined`. The public contract is untouched — no signature
narrowed, no guardrail changed, callers outside this file unaffected.
280 -> 268, zero new, on a line-number-agnostic diff of the full tsc error set.
Tests: the `void` arm of the documented contract had NO coverage —
guardrails-registry.test.ts exercises guardrails that return a result and one
that throws, never one that returns nothing. Added
guardrails-void-no-change-contract.test.ts (4 tests): silent preCall/postCall
pass through untouched and are recorded as ran-not-skipped-not-errored;
returning `{}` produces an identical execution record; a silent guardrail does
not stop a later one from modifying. They pass against the parent commit's
registry.ts too, which is the evidence for no behavior change.
75/75 across the 13 guardrail suites.
Co-authored-by: backryun <busan011@ormbiz.co.kr>