* fix(logs): avoid giant provider pills for failed auto family requests
* refactor(logs): extract resolveRejectedComboProvider + cover it
The provider label was decided inline in handleChat, which has no test harness —
the change shipped untested and pushed chat.ts over its frozen size (1848 > 1845).
Moved to rejectedRequestUsage.ts next to summarizeComboAttemptedModels, the helper
it replaces on this path. chat.ts shrinks back under its baseline (no rebaseline
needed) and the logic gets three cases in the suite that already covers its sibling:
auto/* collapses to "auto", a named combo keeps its name, and bare "auto" (no
slash) is NOT collapsed — that one is a combo request, not a family request.
Also rebased on the current release tip and added the changelog fragment.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Co-authored-by: rafaeldrincon <rafaeldrincon@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Second aggregation pass, for the PRs merged after the main reconciliation:
#8860, #8861, #8862, #8863, #8865, #8866.
The `### 📝 Maintenance` heading added to the living section in 26a7783521 held —
[3.8.47] stays at its 234 bullets instead of absorbing this cycle's maintenance
entries. Credits attached where the fragment carried the number only in its
filename: @HouMinXi (#8860, #8865), @artickc (#8863), @rafaeldrincon (#8866).
#8862 already carried its own link and credit.
[3.8.49]: 1172 -> 1178 bullets · contributors 155 -> 156 · 42 i18n mirrors synced.
* fix(adobe-firefly): default gpt-image detailLevel to maximal (5)
GPT Image 2 quality is dominated by generationSettings.detailLevel (1-5).
The SPA often defaults to 3 (medium); missing/auto quality previously mapped
to 3 as well. Default now to 5 (high/max) so API clients and Media without
an explicit quality still get maximal detail. Explicit low/medium still honored.
* chore(quality): rebaseline adobeFireflyClient + changelog fragment
adobeFireflyClient.ts 2317->2322 (+5) — this PR's own growth at the existing
payload-build site. Covered by tests/unit/adobe-firefly.test.ts.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
* fix(autoRouting): recognize auto/\<family\> combos in classifyAutoModel
classifyAutoModel() checks VALID_AUTO_VARIANTS and parseAutoSuffix but
never isValidModelFamily, so auto/glm, auto/minimax, auto/llama etc. are
rejected as "Unknown built-in auto combo" before chatHelpers.ts or
builtinCatalog.ts can handle them.
Fix: import isValidModelFamily and ModelFamily, add family to spec type,
check family suffixes before returning unrecognized. Mirrors the pattern
already in builtinCatalog.ts createBuiltinAutoCombo.
Closes: auto/\<family\> combos listed in /api/combos/auto but unusable
at /v1/chat/completions.
* test(autoRouting): cover auto/<family> classification + changelog fragment
The PR changed production code with no test — nothing in tests/ referenced
classifyAutoModel. Since it is module-private, the new suite exercises it through
the public resolveAutoRoutingState().
Verified it guards something real: against the release tip without this fix the
family case fails ("auto/glm should be a recognized built-in auto model"), and
passes with it. Also pins that a category suffix does not pick up spec.family and
that an unknown suffix stays unrecognized.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: rafaeldrincon <rafaeldrincon@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(combo): clean up stale connectionId refs after provider delete
Deleting a provider connection left stale connectionId references in
combo route models, causing the dashboard to show deleted providers.
Add cleanupComboConnectionRefs to scan combos and null out any
connectionId or allowedConnectionIds entry matching the deleted connection.
Call it from the DELETE handler alongside the existing synced-model cleanup.
Signed-off-by: Minxi Hou <houminxi@gmail.com>
* fix(types): widen the combo-step cast + changelog fragment
typecheck:core rejected `combo.models as Record<string, unknown>[]` with TS2352
— ComboStep[] and Record<string, unknown>[] do not overlap enough for a direct
assertion. Goes through `unknown`, as the compiler suggests.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Signed-off-by: Minxi Hou <houminxi@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(oauth): support Test Connection for xAI OAuth
Wire xai-oauth / xao into OAUTH_TEST_CONFIG with the shared api.x.ai
chat probe so dashboard Test Connection no longer returns
"Provider test not supported" on healthy accounts.
* chore(changelog): add changelog for xai-oauth test connection support
---------
Co-authored-by: allanvb <allanvb@users.noreply.github.com>
* feat(sse): add alternateFormats registry field and resolver
* feat(sse): honor per-connection targetFormat in getTargetFormat
Registry-driven format lookup now resolves an alternate protocol
declared for the provider when the connection's providerSpecificData
carries a matching targetFormat, falling back to the entry's default
format otherwise.
* feat(sse): resolve base URL from selected alternate format
resolveBaseUrl now falls back to the connection's selected alternate
protocol (providerSpecificData.targetFormat) before the provider's
default base URL, while a manual providerSpecificData.baseUrl override
still wins over both.
* feat(sse): apply alternate format auth header and extra headers
DefaultExecutor's registry authHeader lookup and BaseExecutor's shared
header preamble now both honor a connection's selected alternate
protocol: the alternate's authHeader wins over the registry default,
and its extra headers (e.g. Anthropic-Version) are merged in.
* refactor(sse): extract resolveAlternate helper into BaseExecutor
Centralizes the getRegistryEntry() + resolveAlternateFormat() pair
that resolveBaseUrl, buildHeadersPreamble, and DefaultExecutor's
authHeader lookup each duplicated, so a future call-site can't diverge
from the shared precedence. Also translates the PT-BR comments added
in the previous three commits to match the surrounding English. Pure
refactor — no behavior change.
* feat(sse): declare Anthropic-compatible variant for xiaomi-mimo
The provider publishes the same catalog over /anthropic/v1/messages on the same
host. Selecting it also required bypassing the per-provider URL normalizers in
DefaultExecutor.buildUrl(): normalizeXiaomiMimoChatUrl() appends /chat/completions
unconditionally, which mangled the alternate's already-complete endpoint into
.../anthropic/v1/messages/chat/completions.
* feat(sse): add xiaomi-mimo-token-plan provider with monthly quota
Token Plan is a separate product: tp- keys authenticate only on the regional
token-plan-sgp host and return 401 on api.xiaomimimo.com, where the existing
xiaomi-mimo provider points. Same pattern as qwen-cloud-token-plan.
Registers the monthly token allowance (no balance API upstream) and declares
the Anthropic-compatible variant on the token-plan host.
* feat(dashboard): add API protocol selector to connection modal
Providers that declare alternateFormats in the registry now expose an opt-in
protocol dropdown on the connection modal. The choice persists to
providerSpecificData.targetFormat as an explicit null when set back to the
default, since the PUT route merges { ...existing, ...incoming } and an omitted
key would keep the previous override.
* fix(i18n,quality): vi parity for the protocol selector + own-growth rebaselines
The three new provider keys landed only in en/pt-BR, so the vi parity test failed
(tests/unit/i18n-vi-completeness.test.ts asserts key parity AND no __MISSING__
markers — running i18n:sync-ui would have satisfied the first and broken the
second). Added translated values instead. Scoped to vi: it and pt-BR are the only
locales with a parity test.
Rebaselines are this PR's own growth: EditConnectionModal.tsx 1283->1316 (the
selector field) and open-sse/executors/base.ts 1540->1562 (alternate-format
resolution at the existing buildUrl/headers chokepoint).
Adds the changelog fragment.
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
* fix(token-refresh): discover projectId during token refresh
The token refresh path (tokenRefresh.ts) did not discover projectId
for antigravity/agy accounts. Dashboard and health check refresh use
this path, not the executor path.
Add ensureAntigravityProjectAssigned call after refreshGoogleToken
for antigravity/agy providers when projectId is empty.
Signed-off-by: Minxi Hou <houminxi@gmail.com>
* chore(quality): rebaseline the token-refresh test file + changelog fragment
tests/unit/token-refresh-service.test.ts 1311->1378 (+67) — the four cases
covering projectId discovery on the tokenRefresh.ts path. Growth is the tests
this PR adds, nothing else.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Signed-off-by: Minxi Hou <houminxi@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Aggregates every pending changelog.d fragment into the [3.8.49] section and
regenerates the contributors table from the reconciled bullets.
Three fixes this surfaced:
- The [3.8.49] section had no `### 📝 Maintenance` heading, so the aggregator's
findIndex matched the first one in the file — inside [3.8.47] — and would have
filed 92 maintenance bullets under the wrong release. Added the heading to the
living section; [3.8.47] stays at its original 234 bullets.
- 46 bullets carried no PR/issue reference. Fragments may keep the number only in
the filename (`<N>-slug.md`), which the aggregator does not copy into the bullet,
so the link and the credit were dropped on aggregation. Restored, scoped strictly
to the [3.8.49] range.
- 9 external contributors lost their attribution that way and are credited again:
@MisileLab (#8566), @MumuTW (#8619), @epsilonode (#8724), @hppsc1215 (#8835),
@sumanxg (#8837, #8856), @TitoTFP (#8838), @HouMinXi (#8842, #8845).
Contributors table: 84 → 155 entries, no one removed. 42 i18n mirrors synced.
check:changelog-integrity green — no base bullet lost.
* fix(cli): escape codex args and stop aborting on exit in launch-codex
`launch-codex` spawns `codex.cmd` with `shell: true` on Windows, so Node joins
argv with plain spaces and no escaping (DEP0190). This mangles every Windows
invocation, not only the ones with a multi-word user argument, because the
injected `-c` provider flags carry quoted TOML values:
["-c","model_provider=omniroute", ...,
"model_providers.omniroute.base_url=http://localhost:20128/v1",
"fix","the","bug"]
cmd.exe strips the TOML quotes (`model_provider=omniroute` no longer parses as
a TOML string), splits multi-word arguments, and swallows everything after an
unquoted `&`. The same defect was fixed for `launch` in #8837; this ports it to
`launch-codex`, which that PR disclosed but left unfixed.
- extract the escaping into `bin/cli/utils/winShellArgs.mjs` and reuse it from
both launchers instead of keeping a private copy in `launch.mjs`
- quote the codex argv (provider flags + profile + pass-through args) on the
win32 shell path; argv is untouched off Windows, where no shell is involved
- replace `process.exit()` in the command action with `process.exitCode`: on any
non-zero child exit it aborted with the libuv `!(handle->flags &
UV_HANDLE_CLOSING)` assertion while the inherited stdio handles were closing
Test: `tests/unit/cli/launch-codex-windows-spawn-args.test.ts` pins the exact
encoding with golden strings (the cmd.exe round-trip is Windows-only and skips
on Linux CI, so without goldens CI would guard nothing) and round-trips the real
provider flags through a probe `.cmd` shim that forwards `%*`.
* docs(changelog): add fragment for #8856
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(executors): route current Claude generations through Vertex partner endpoint
PARTNER_MODELS pinned three Claude 3.x prefixes (claude-3-5-sonnet,
claude-3-opus, claude-3-haiku). Every newer Claude generation on Vertex
(claude-sonnet-4-6, claude-haiku-4-5, etc.) fell through to the
Google-publisher branch instead, producing an invalid
publishers/google/models/claude-... path.
Replace the pinned prefixes with a single generic "claude-" prefix:
any Claude model on Vertex is always an Anthropic partner model, never
a Google one, so this can't go stale again the way pinned version
strings did.
Fixes#1985
* docs: add changelog fragment for #8852
* fix(ci): raise the git ls-files buffer in check:tracked-artifacts
`execFileSync` defaults to a 1 MiB stdout buffer and throws ENOBUFS past it.
`git ls-files -s` on this repo is already at 1,042,494 bytes across 11,091
tracked files — 6,082 bytes from the ceiling. Any PR adding roughly sixty files
crosses it.
That matters more than a failing script: the check runs on pre-commit, so once
the listing crosses 1 MiB, committing breaks for everyone working the repo, not
just for the change that happened to cross it. It is not a hypothetical — the
private EE fork hit it this week when a sync landed ~214 translation files and
pushed the listing 504 bytes over; every commit there failed the hook until
this same fix landed.
Both call sites now share a GIT_LS_OPTS with a 64 MiB ceiling — far above any
plausible tree, rather than just above today's, since the listing only grows.
* docs(changelog): add fragment for #8844
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* feat(oauth): support web client type for remote Google OAuth
Google Desktop app OAuth clients require loopback redirect URIs per
policy, which breaks remote deployments where the browser cannot reach
127.0.0.1 on the server. Add ANTIGRAVITY_OAUTH_CLIENT_TYPE env var:
when set to 'web' and OMNIROUTE_PUBLIC_BASE_URL is configured, the
loopback redirect URI is upgraded to the public base URL. Default
behavior (desktop) is unchanged.
Enables remote deployments without SSH tunneling by registering a Web
application OAuth client in Google Cloud Console.
Signed-off-by: Minxi Hou <houminxi@gmail.com>
* docs(changelog): add fragment for #8845
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Signed-off-by: Minxi Hou <houminxi@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(antigravity): discover projectId during token refresh
The initial OAuth exchange can fail to populate projectId via
loadCodeAssist (network timeout, account not yet onboarded). The
runtime transformRequest path already recovers via
ensureAntigravityProjectAssigned, but refreshCredentials did not --
after a token refresh the per-token memoization cache is invalidated
(new access token = new cache key), so every subsequent request
triggers a fresh loadCodeAssist round-trip that may fail again.
Add a best-effort ensureAntigravityProjectAssigned call in
refreshCredentials when projectId is empty, mirroring the pattern in
transformRequest. Persist the discovered id so it survives the next
refresh or restart.
Signed-off-by: Minxi Hou <houminxi@gmail.com>
* chore(quality): rebaseline antigravity.ts and test file-size
antigravity.ts grew from 1493 to 1528 lines (+35) with projectId
discovery in refreshCredentials. executor-antigravity.test.ts is a new
test file at 1098 lines (above cap 1000) with 4 new test cases.
Signed-off-by: Minxi Hou <houminxi@gmail.com>
* fix(antigravity): match ExecutorLog arity in the refresh discovery log
`ExecutorLog.info` (open-sse/executors/base.ts) is `(tag, message) => void` —
two parameters. The discovery log passed a third metadata object, which failed
typecheck:core with TS2554 on antigravity.ts:777. Bind the message to a local
and pass two arguments, matching the sibling warn on the catch branch. Kept to
two lines so the file stays at its frozen size; ran Prettier, which also wrapped
the pre-existing over-width `const msg` line below.
Also adds the changelog fragment for the fix.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Signed-off-by: Minxi Hou <houminxi@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(cli): escape claude args and stop aborting on exit in omniroute launch
Windows launches go through spawn(..., { shell: true }), which joins argv
with plain spaces and no escaping (Node DEP0190). Any argument containing a
space was split, so `omniroute launch -p "two words"` reached claude as
`-p two` plus stray positionals, and the prompt was silently truncated.
Escape each argument for cmd.exe instead: CRT argv rules first (double the
backslashes preceding a quote, escape embedded quotes, wrap in quotes), then
cmd metacharacters caret-escaped twice. The second pass is required because
claude.cmd is an npm shim that re-parses %* on the way to node; with a single
pass arguments still truncated at the first `&` or `|`.
The command action also called process.exit() on any non-zero exit. That tore
the loop down while the exited child's inherited stdio handles were still
closing and aborted the process with a libuv assertion
(!(handle->flags & UV_HANDLE_CLOSING), src/win/async.c:94, exit 0xC0000409)
instead of returning claude's exit code. Set process.exitCode and let the
loop drain.
Extracts resolveClaudeSpawn() alongside the existing resolveCodexSpawn()
precedent so both the platform choice and the escaping are unit-testable.
Tests: tests/unit/cli/launch-windows-spawn-args.test.ts (new, 7 tests).
Four pure-function tests plus golden strings pin the exact encoding on every
platform; a Windows-only test round-trips argv through a real npm-style .cmd
shim and asserts embedded quotes, `&`, `|`, `%PATH%`, `^`, `!`, a trailing
backslash and an empty string all arrive byte-identical.
Note: bin/cli/commands/launch-codex.mjs carries the identical defect (same
shell:true concatenation, same process.exit) and is left unchanged here.
* docs(changelog): add fragment for #8837
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(ghe-copilot): route OpenAI-native models via Responses API
- Add targetFormat: 'openai-responses' to gpt-5.4-mini, gpt-5.3-codex, gpt-5-mini,
mai-code-1-flash, and oswe-vscode-prime in ghe-copilot registry.
- Register gpt-5.6-sol, gpt-5.6-terra, and gpt-5.6-luna with openai-responses targetFormat.
- Update GheCopilotExecutor.buildUrl() to route openai-responses and codex models to
<gheUrl>/responses while keeping Claude and Gemini on /chat/completions.
- Add unit tests verifying targetFormat parity and buildUrl endpoint routing.
* docs(changelog): add fragment for #8835
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: Alex <sefias_methue@hotmail.de>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(providers): correct Codex GPT-5.6 context window (#7702)
* docs(changelog): add fragment for #8838
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* test(providers): align the remaining GPT-5.6 context-window assertions
Three suites assert the pinned GPT_5_6_CODEX_CAPABILITIES contract through the
VS Code and provider-models routes, and still expected 372000. They only surface
in a full run, so the focused loop on this PR stayed green while `npm run test:unit`
failed with five `272000 !== 372000`.
The two conservative-merge cases in provider-models-route-codex keep testing what
they tested: live 999999 still exceeds the pinned value (pinned wins) and live
100000 is still below it (live wins). Only the pinned number and the comments
naming it move.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(api): stop /v1/models rebuilding the catalog on almost every request
The response cache added by #6408 memoized the serialized body for
`modelCatalogCacheTtlMs`, defaulted to 1500 ms. On a real install the builder
takes far longer than that: measured on the production VPS, ~49 s for a 1.3 MB /
2645-model catalog. Any two requests more than 1.5 s apart therefore both missed
the fresh window, and the second fell into stale-while-revalidate — which
rebuilds via `setTimeout(…, 0)` and, because the builder is overwhelmingly
synchronous under the single-threaded App Router, pins the event loop, so even
the "served immediately" stale body only reaches the client once the rebuild
finishes. Net effect: ~50 s on essentially every call.
Measured on the VPS (1 cold build + 5 sequential requests):
cold = 48.93s
req1 = 3.19s ← the only hit
req2 = 50.51s req3 = 50.93s req4 = 47.43s req5 = 52.28s
Raise the default to 60 s. A short TTL is redundant with the invalidation this
cache already has: `invalidateDbCache()` bumps `modelCatalogCacheVersion` on
every settings/connections/combos/pricing write and
`dropCatalogCacheIfStateChanged()` drops the whole cache the moment it moves, so
post-write freshness never depended on the TTL. What the TTL governs is the
"nothing was written" case, where replaying a body built seconds ago is the
point of the cache. 60 s matches the ceiling the settings schema already allows
for the override, so the default can never exceed what an operator may configure.
The value that actually takes effect is the settings default, not the constant:
`catalog.ts` resolves `dbSettings.cache?.modelCatalogCacheTtlMs ??
CATALOG_CACHE_TTL_MS_DEFAULT`, and the `??` never falls through while a settings
default is declared. Raising only the constant is a silent no-op — which is how
the first attempt at this fix measured identical to no fix at all. All three
declarations are aligned and a test pins them together.
* docs(changelog): add fragment for #8833
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Adds GET /api/usage/cache-health and a Cache Health tab under
/dashboard/analytics, both backed by a pure summary over the cache columns
already present in call_logs.
Motivated by a production diagnosis where the aggregate ratio was actively
misleading. The window read 24.8M cached tokens and wrote 9.5M — a
write/read of 0.385, which reads as merely mediocre. The actual shape was
very different: the median call wrote 848 tokens while 18% of the calls
carried 94% of every written token, and two models in the same window sat
at 0.13 (Sonnet) and 0.51 (Opus). Averaging hid all three facts.
So the summary reports what the average cannot: the distribution
(p50/p90/p99), the concentration (how few calls carry how much of the
write), and the per-model split. The heavy-write threshold is relative to
the window (10x the median, floored at 1024) because a cutoff tuned for
130k-token conversations reports nothing at all on 2k-token ones; 1024 is
the minimum Anthropic bills for cache creation, below which a write carries
no signal.
Calls that neither read nor wrote are counted separately from thrash — a
route that does not cache is an absence of caching, not a sick cache — and
only successful calls are summarized, since a 4xx/5xx never reached the
provider cache and would dilute the ratio.
Tests cover the summary (8) and the route (6, against a real SQLite so the
WHERE clause itself is exercised), including that an internal failure
answers 500 without leaking the stack trace, the SQL text or a table name.
The handler awaited the DELETE and never looked at the response:
if (!confirm(t("removeConfirm"))) return;
await fetch(`/api/quota/pools/${id}`, { method: "DELETE" });
await mutate();
When the request failed — a 401 from an expired session, a 500, a dropped
connection — the page revalidated, the card stayed exactly where it was, and
nothing was shown. From the operator's side the click was indistinguishable
from a misclick, so the natural reaction is to click again. The page had no
error surface at all, unlike the sibling flow in the API manager which already
checked res.ok and rendered the message.
Adds a dismissible alert above the header, fed by both failure paths: a
non-ok response (appending the API's message when it sends one) and a thrown
request, which has no response to read. The alert clears on the next attempt,
so a transient failure does not leave a stale error on screen.
Reported against two boxes on 2026-07-28. The delete itself was working there
— the audit log shows the pools were removed — which is exactly what this
change makes visible either way.
Tests (vitest/jsdom, 5): non-ok response, thrown request, success path stays
quiet and still revalidates, a dismissed confirmation issues no DELETE at all,
and an earlier error clears once a later delete succeeds.
* docs: restore the Polish API_REFERENCE removed by #8823
`docs/reference/API_REFERENCE.md` lists Polish in its language index, but the
target file no longer exists: #8823 ("replace outdated Polish docs with
translation from latest English") deleted
`docs/i18n/pl/docs/reference/API_REFERENCE.md` without writing a replacement.
Every other one of the 31 linked languages still has the file.
That leaves `check:doc-links` red — and because the Docs Gates job only runs on
pull requests, the branch itself reports green while carrying the break. The
first PR opened afterwards is what surfaces it, and it blocks every PR until
fixed.
Restored from `71c5a7592^`. The content is the pre-#8823 translation, which is
by definition outdated relative to the English source — but an outdated
translation behind a working link is strictly better than a 404, and the next
translation pass will overwrite it. `check:doc-links` now passes (813 internal
links, 0 broken).
The same commit also removed four other Polish files
(`cloudflare-zero-trust-guide.md`, `features/context-relay.md`,
`reference/CLI-TOOLS.md`, `reference/ENVIRONMENT.md`). None of them is linked
from a scanned index, so none breaks the gate; they are noted here so a later
translation pass can decide whether they should come back too.
* chore(quality): freeze the pre-existing exhaustive-deps error in ProviderAccountRoutingCard
Second half of unbreaking the base. `lint:json --max-warnings 0` fails on
`ProviderAccountRoutingCard.tsx:87` — `save` calls `load()` but declares only
`[providerKey]`. Same root cause as the broken doc link in the previous commit:
the Lint job is skipped on pushes to `release/**`, so the branch reports green
while carrying the violation, and every PR inherits it.
Frozen rather than fixed: adding `load` to the dependency array changes when
the callback is recreated in a settings component, and that belongs in a change
that can verify the card's behaviour. The entry keeps the gate meaningful for
genuinely new warnings instead of leaving it red for everyone.
With this and the restored Polish file, both gates pass on the base again.
* fix(reasoning): sanitize streamed K3 think tags
* refactor(stream): move think-tag helpers into thinkTagParser leaf module
open-sse/utils/stream.ts is frozen at 2887 lines with zero headroom, and the
kimi-coding-apikey think-tag handling added here pushed it to 2951. The parsing
half of that work has no SSE dependency, so it moves to thinkTagParser.ts (an
unfrozen leaf): the open-tag lookahead predicate and the end-of-stream flush
delta assembly. stream.ts keeps only the SSE envelope - enqueue, payload
collection, logging.
Getting back under the frozen ceiling needed more than just the PR's own
added lines, since even the fully self-gating helpers still cost a handful of
call-site lines stream.ts has zero room for. Along the way this also
deduplicates a synthetic chat-completion-chunk literal that was copy-pasted
three times in stream.ts (textual tool-call flush, think-tag flush, terminal
finish_reason synthesis) into one buildSyntheticChatChunk() in
streamHelpers.ts - pure DRY, no behavior change.
Behavior is unchanged: same tests, same counts.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: rinseaid <rinseaid@rinseaid.net>
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* test(quality): fail loudly when a source-scanning guard is negative-only
A negative guard — assert.doesNotMatch(src, /x/) or src.includes(x) === false —
passes against an empty string. Once the code it guards is extracted into another
file the parent no longer contains the string, so the assertion keeps passing while
protecting nothing. The regression coverage is deleted with no test turning red,
which is exactly the failure mode the god-file decomposition campaign (#8617) is
about to trigger 90-odd times.
Adds tests/unit/source-scanner-guards.test.ts: a hard gate (no baseline, no
allowlist) requiring every test variable bound to project source to carry at least
one positive anchor. Classification runs on logical statements with strings, regexes
and comments blanked out, so a guard wrapped across lines cannot slip past — that
folding is what exposed 3 of the 7 violations.
Fixes all 7 violations across 6 files with one stable top-level export anchor each.
Two were security scope guards held only by multi-line negative assertions: the SSRF
guards on /api/sync/initialize (#323) and the proxy-bypass guards on chatHelpers.ts
and chatCore.ts (#3226) — the latter anchored on handleChatCore precisely because
that file is a decomposition target.
Adds tests/_helpers/readSrc.ts, a repo-root-relative reader that throws on a missing
or empty file instead of returning "".
Refs #8617
* docs(changelog): number the fragment for #8619
* chore(skills): sync cli-backup-sync SKILL.md with catalog
Same tip fix as #8657 so Merge integrity is green without waiting for
that PR to land. Regenerated via generate-agent-skills --apply.
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.
CodeQL js/polynomial-redos (alerts #765/#766).
`/\/+$/` has no left anchor, so the engine retries the match at every
start offset and each attempt re-walks the whole slash run before failing
`$` — O(n^2) on a connection baseUrl made of many slashes. Measured on
the vulnerable code: 10k slashes = 102ms, 30k = 968ms, 60k = 4028ms
(clean quadratic); through the public resolvers the same input took 22s.
providerSpecificData.baseUrl is operator-supplied config and reaches the
trim via isFamilyPresetUrl() -> normalizeEndpoint() and both resolve*Url()
helpers, so the input is reachable.
Replaces all three identical occurrences with a linear charCodeAt scan.
CodeQL only flagged two of them; the third (resolveAlibabaProviderModelsUrl)
carries the same defect and is fixed here rather than left behind.
Behavior is unchanged: every trailing slash is still removed (not a bounded
subset), interior slashes are preserved, and an all-slash string still
collapses to empty — asserted by the new behavior test.
* fix(api): enforce image generation API key auth
* fix(api): align image route auth guard with clientApiPolicy
The route-level guard added for image generation was stricter than the
authz middleware that already fronts /api/v1/* (src/proxy.ts →
clientApiPolicy), so requests the pipeline admits were 401'd by the
handler:
- A cookie-authenticated dashboard session was rejected under
REQUIRE_API_KEY=true. The dashboard Media page
(dashboard/cache/media) and the Playground call these routes with a
session and no Bearer — the same mismatch already fixed for
/api/playground/presets.
- A presented invalid key was rejected even with REQUIRE_API_KEY=false,
where clientApiPolicy (#2257) and the sibling /v1/embeddings and
/v1/web/fetch routes degrade a stale CLI key to anonymous instead.
Extract the shared guard into shared/utils/clientApiRouteAuth so both
image routes (and future /v1 handlers) mirror the middleware contract
instead of re-deriving it, and drop the now-dead auth imports.
Also switch the call-log attribution fallback back to `||`: with `??`,
an empty-string apiKeyId/apiKeyName would be persisted verbatim and
would block the request-scoped context, which the previous
`entry.apiKeyId || null` never did.
Tests: cover the dashboard-session and keyless-mode-invalid-key
branches, and split the auth/attribution cases into
image-generation-route-auth.test.ts to stay under the 800-line
new-test-file cap.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: alexey.nazarov@softmg.ru <alexey.nazarov@softmg.ru>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
removeNativeModules() was called with a hardcoded ".next" path while the actual
distDir is NEXT_DIST_DIR (".build/next" by default). Because the function
early-returns when the directory does not exist, the cleanup silently no-opped
and the plain-Node-ABI better-sqlite3 copy produced by `next build` survived
into the packaged app.
At runtime the standalone server runs under ELECTRON_RUN_AS_NODE, so it needs
the Electron ABI (148 for electron 43). Loading the ABI-137 copy fails with
ERR_DLOPEN_FAILED, the app falls back to the sql.js WASM driver, the connection
is closed and retried in a loop, WASM memory is never reclaimed and the process
OOMs -> HTTP 500 on every route.
Also adds assertNoStaleHashedNatives() so a wrong baseDir fails the build
instead of silently shipping a broken installer. This has regressed at least
twice (#1497 with ABI 127 vs 145, #7082/#7681 with 137 vs 148).
Refs #7082, #7681, #1497, #8792. Supersedes the abandoned #7123.
Plugins always received a hard-coded {status:200} stub, so hooks like
request-logger never saw response bodies. Forward translated JSON for
non-streaming completions and a streamed flag for SSE without reading
the body twice.
Closes#8711
Dashboard Codex restrictions use the public cx/ alias while /v1/responses
normalizes bare Codex IDs to codex/, so allow/block checks falsely 403'd.
Expand permission candidates via the provider registry alias map.
Closes#8803
Extract ModelTable from the charts god-file and drive header clicks through a
single sort state object plus a pure sorter so column toggles reliably reorder
rows. Compute missing share pct from summary totals when the analytics API
omits it.
Api-key 403 bodies with Cloudflare error 1010 / browser_signature_banned /
retryable:false were treated as short AUTH_ERROR cooldowns, so the chat loop
waited ~21–33s before falling through. Return cooldownMs:0 so the tier fails
fast without permanently banning the account.
Strict contextFilterMode excluded every target whose context limit was missing
from the capability catalog, so otherwise-executable combos returned 404
no_executable_targets. Restore unknown-context targets when no known-good
survivor remains, surface context_requirements_exhausted from targetResolution,
and keep the empty-pool payload in pinRecovery after the #8592 split.
#8595 (compact Responses multi-turn images before the context hard-reject)
grows open-sse/handlers/chatCore.ts 4955 -> 5006. The growth is irreducible at
the existing compaction chokepoint: a last-resort retry against the concrete
token budget plus the estimateFinalInputTokens helper, both wired into the
pre-existing call site rather than a new branch. Covered by
tests/unit/8560-responses-image-compaction.test.ts.
* fix(client): rewrite absolute fetch/EventSource paths under basePath
Absolute browser calls like fetch("/api/...") and new EventSource("/api/...")
do not honor Next.js basePath, so subpath deploys (OMNIROUTE_BASE_PATH) break
dashboard health checks, settings APIs, and SSE unless a reverse proxy rewrites
the domain root.
- Add withBasePath / getDeployBasePath helpers
- Install ref-counted fetch + EventSource rewrite when basePath is set
(same pattern as installDashboardCsrfFetch)
- Mount BasePathNetworkProvider at the root so login works too
- Mirror OMNIROUTE_BASE_PATH to NEXT_PUBLIC_OMNIROUTE_BASE_PATH for the client
- Document in .env.example; unit tests for rewrite rules
* docs(changelog): add fragment for #8515 basePath client fetch
* test(client): move basePath tests into a scanned dir and fix no-op call-shape asserts
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(client-sweep-8515): restore CHANGELOG #8471 bullet and fix basePath TS2322
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: rqzbeh <rqzbeh@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: diegosouzapw <diegosouzapw@users.noreply.github.com>
* fix(hyperagent): sticky thread for agentic tool loops (Claude Code)
Follow-up to #7994. When a reverse-conversion proxy rewrites assistant
text between turns (Intent+JSON -> native tool_calls -> re-serialized tool
text), conversationFingerprint(prefix) no longer matches the key stored
after turn 1, so HyperAgent created a new thread and multi-turn tool
results appeared as a cold start.
- Key sticky sessions by root user task (normalize pin wrappers)
- Flatten Anthropic tool_use / tool_result for lastUserText + fingerprints
- Regression tests for mutated-assistant tool loops
Tests: tests/unit/executor-hyperagent.test.ts (19/19)
* chore(quality): rebaseline for PR #8470 own-growth (hyperagent sticky thread)
open-sse/executors/hyperagent.ts grows 937->1026 lines and gains one new
cognitive/cyclomatic-complexity violation (extractMessageText, from the
new Anthropic tool_use/tool_result flattening branches) on top of
inherited base-tip drift already present on origin/release/v3.8.49
(file-size was already at cap; cognitive-complexity 951->956 and
cyclomatic-complexity 2130->2169 drift predates this PR). Rebaselined
file-size to 1026, cognitiveComplexity to 957, complexity count to 2170,
matching measured values on the merged tree.
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: diegosouzapw <diegosouzapw@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Fixes#8788.
- Added 'UND_ERR_SOCKET' to PROXY_UNREACHABLE_ERROR_CODES so socket disconnects and mid-stream closes are properly tagged with code PROXY_UNREACHABLE and errorCode proxy_unreachable.
- Ensured tagProxyUnreachable normalizes error code to PROXY_UNREACHABLE and errorCode to proxy_unreachable when catching socket-level disconnects.
- Added test in proxyfetch-undici-retry.test.ts verifying UND_ERR_SOCKET classification.
* fix(sse): compact Responses multi-turn images before context hard-reject (#8560)
Codex Desktop sessions near the 372k input cap were rejected on the second
inline image because compressContext no-op'd on Responses input[] and never
pruned older vision turns. Adapt via bodyAdapter, prune older images while
keeping the latest, and run last-resort compaction before the budget check.
* docs(env): document CONTEXT_KEEP_LATEST_IMAGES for #8560
Keep check:env-doc-sync green after the context image-pruning override.
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>