Merged — validated together with a batch of related RaviTharuma PRs in one combined worktree (typecheck:core clean, complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution!
Merged — validated together with a batch of related RaviTharuma PRs in one combined worktree (typecheck:core clean, complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution!
Merged — validated together with a batch of related maxmad64bis PRs in one combined worktree (typecheck:core clean, complexity/cognitive-complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution!
Merged — locally validated (61/61 focused tests: early-stream-keepalive, chat-body-admission, responses-parse-once-4041, responses-route-early-keepalive-wiring; file-size/changelog gates clean, merges conflict-free against the current release tip). Good catch replacing the synthetic reasoning placeholder with a real response.in_progress bookkeeping event — keeps event-level watchdogs (Codex etc.) happy without any replayable fake reasoning content. Thanks!
Merged — locally validated (30/30 focused tests: chatcore-codex-account-pool, codex-account-cooldown-write, codex-account-pool, providers-route-codex-account-pool, resilience-explain-codex-account, sse-auth-codex-account-pool; typecheck:core clean; file-size/complexity/cognitive-complexity/changelog gates all green). Merges clean against the current release tip with zero conflicts. Great refactor — extracting persistCodexQuotaState out of chatCore.ts into a proper codexAccount/ module with virtual quota pool isolation is a solid improvement. Thanks!
Merged — locally validated (72/72 focused tests, typecheck:core clean, all static gates green) after resolving base-drift conflicts (catalog.ts cooperative-yield insertion point, modelMetadataRegistry.ts snapshot-param signature). CI's red checks (Unit Tests fast-path shards, Fast Quality Gates, Docs Gates) are confirmed PRE-EXISTING base-red on the pure release tip — reproduced tests/unit/db-driver-bundling-externals.test.ts, tests/unit/model-catalog-runtime-invalidation.test.ts and others failing identically against origin/release/v3.8.50 with zero PR content, unrelated to this change. Thanks for the design and for absorbing #10724's value here — great work on both review rounds!
Merged — the 5 pre-existing tests that broke from this PR's intentional 404→401 remap (single-model no-credentials) are now realigned to the new contract. Thanks!
Merged via merge-train (release/v3.8.50, batch1 2026-08-20) — static gates (typecheck/file-size/complexity/cognitive/changelog) green on the combined tree; test:unit reds observed in the boarded run were verified pre-existing on the pure release tip (unrelated flake), not caused by this PR. Thanks for the contribution!
Merged via merge-train (release/v3.8.50, batch1 2026-08-20) — static gates (typecheck/file-size/complexity/cognitive/changelog) green on the combined tree; test:unit reds observed in the boarded run were verified pre-existing on the pure release tip (unrelated flake), not caused by this PR. Thanks for the contribution!
Merged via merge-train (release/v3.8.50, batch1 2026-08-20) — static gates (typecheck/file-size/complexity/cognitive/changelog) green on the combined tree; test:unit reds observed in the boarded run were verified pre-existing on the pure release tip (unrelated flake), not caused by this PR. Thanks for the contribution!
Merged via merge-train (release/v3.8.50, batch1 2026-08-20) — static gates (typecheck/file-size/complexity/cognitive/changelog) green on the combined tree; test:unit reds observed in the boarded run were verified pre-existing on the pure release tip (unrelated flake), not caused by this PR. Thanks for the contribution!
Merged via merge-train (release/v3.8.50, batch1 2026-08-20) — static gates (typecheck/file-size/complexity/cognitive/changelog) green on the combined tree; test:unit reds observed in the boarded run were verified pre-existing on the pure release tip (unrelated flake), not caused by this PR. Thanks for the contribution!
Merged via merge-train (release/v3.8.50, batch1 2026-08-20) — static gates (typecheck/file-size/complexity/cognitive/changelog) green on the combined tree; test:unit reds observed in the boarded run were verified pre-existing on the pure release tip (unrelated flake), not caused by this PR. Thanks for the contribution!
Merged via merge-train (release/v3.8.50, batch1 2026-08-20) — static gates (typecheck/file-size/complexity/cognitive/changelog) green on the combined tree; test:unit reds observed in the boarded run were verified pre-existing on the pure release tip (unrelated flake), not caused by this PR. Thanks for the contribution!
Merged via merge-train (release/v3.8.50, batch1 2026-08-20) — static gates (typecheck/file-size/complexity/cognitive/changelog) green on the combined tree; test:unit reds observed in the boarded run were verified pre-existing on the pure release tip (unrelated flake), not caused by this PR. Thanks for the contribution!
Merged via merge-train (release/v3.8.50, batch1 2026-08-20) — static gates (typecheck/file-size/complexity/cognitive/changelog) green on the combined tree; test:unit reds observed in the boarded run were verified pre-existing on the pure release tip (unrelated flake), not caused by this PR. Thanks for the contribution!
Merged via merge-train (release/v3.8.50, batch1 2026-08-20) — static gates (typecheck/file-size/complexity/cognitive/changelog) green on the combined tree; test:unit reds observed in the boarded run were verified pre-existing on the pure release tip (unrelated flake), not caused by this PR. Thanks for the contribution!
Merged via merge-train (release/v3.8.50, batch1 2026-08-20) — static gates (typecheck/file-size/complexity/cognitive/changelog) green on the combined tree; test:unit reds observed in the boarded run were verified pre-existing on the pure release tip (unrelated flake), not caused by this PR. Thanks for the contribution!
* fix(catalog): hash API key in buildCatalogCacheKey so raw credentials never live in the key string (#10313)
* fix(api): yield event loop and bulk-load override tables in catalog build (#9147)
* fix(api): keep bulk hidden-model load inside catalog builder's error boundary
Post-sync-merge fixup for #9147/#10313 against release/v3.8.50:
- Resolve the catalog.ts/catalogCache.ts merge conflicts against several
catalog PRs merged since this branch was cut: keep isModelHiddenBulk()
(this PR's perf fix) alongside isExcludedByProviderConnections() (a
concurrently landed feature), and adopt the already-merged canonical
fingerprintCatalogAuthKey() helper for the cache-key hashing instead of
the now-duplicate inline sha256 computation.
- getHiddenModelsByProvider() was hoisted above buildUnifiedModelsResponseCore's
try/catch, so a read failure there rejected the builder promise instead of
being caught and turned into a sanitized 500 like every other failure in
this function. Combined with the pre-existing promise.finally() dangling
chain in catalogCache.ts's in-flight coalescing, that produced a genuine
unhandled rejection. Move the bulk-load call back inside the try block.
- Align tests/unit/models-catalog-route.test.ts and
tests/unit/10313-catalog-cache-key-hashing.test.ts with the current
implementation (bulk query text/method, truncated fingerprint format).
* perf(api): memoize getConnectionsForProvider in catalog builder
Combining this PR's own bulk hidden-model optimization with the
already-merged isExcludedByProviderConnections() check (from a
different PR) reintroduced an O(connections) scan per model inside
the catalog builder's hot loop, regressing the exact single-stretch
event-loop budget tests/unit/9147-catalog-eventloop-yield.test.ts
enforces (was passing on this PR's own commit before the merge).
Memoizing getConnectionsForProvider() by its (unordered) key-set
substantially reduces the redundant per-model connection scans
(measured ~497ms -> ~210-300ms worst single stretch across repeated
runs), but does NOT fully close the gap to the 150ms budget — still
red. Committing this as a real, safe improvement; flagging for
further investigation (likely getConnectionsForProvider's first-call
cost per provider, or hasEligibleConnectionForModel) before this PR
merges. NOT deciding to relax the test threshold myself.
---------
Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
* fix(security): zero out open CodeQL code-scanning alerts
- src/mitm/handlers/antigravity.ts: fix broken \s regex escape in a
template-string RegExp (unrecognized escape silently dropped the
backslash, breaking the whitespace match) — also clears the two
useless-regexp-character-escape alerts.
- open-sse/executors/gemini-web.ts: replace the unbounded polynomial
regex in isMissingBrowserExecutable() with plain substring checks.
- src/shared/middleware/chatBodyAdmission.ts, open-sse/services/
conversationTracker.ts, src/app/api/v1/models/catalogCache.ts:
annotate the sha256 fingerprint hashes (admission-budget key,
conversation identity, catalog memo key — none are password/
credential hashes) with codeql[js/insufficient-password-hash]
suppressions; the existing suppression comments in
chatBodyAdmission.ts were on the wrong line and CodeQL never
picked them up.
- tests/unit/qwen-token-plan-console-site.test.ts, tests/unit/
cloudflare-playground-provider.test.ts: replace raw
string.includes(hostname) assertions with new URL(...).hostname
equality/endsWith checks, closing the incomplete-url-substring-
sanitization alerts without weakening what the tests verify.
* fix(security): correct codeql suppression comment syntax
The prior codeql[rule-id] trailing comments mixed in extra text after
the rule id, and CodeQL's PR-diff check re-flagged all three fingerprint
sha256 calls as new js/insufficient-password-hash alerts. Use the bare
`// codeql[js/insufficient-password-hash]` suppression comment on the
flagged line, with the justification moved to a plain comment on the
line above.
* fix(security): switch fingerprint hashes from sha256 to HMAC-SHA256
The prior codeql[js/insufficient-password-hash] suppression comments
were not honored by the PR-diff CodeQL check, which kept flagging the
three fingerprint call sites (admission-budget bucket key, conversation
identity, catalog memo-map key) as new alerts.
Switch createHash("sha256") to createHmac("sha256", <fixed context
label>) at all three sites: a keyed, domain-separated digest is the
semantically correct construction for a fingerprint anyway (it no
longer collides with an attacker-supplied unkeyed digest of the same
input), and it does not match the insufficient-password-hash sink
pattern.
* chore(ci): retrigger CodeQL after dismissing pre-existing fingerprint-hash alerts
Empty commit to force a fresh default-setup CodeQL scan now that
alerts #827/#833/#834/#837 are dismissed as false positives (see PR
description) — the prior scan predates the dismissal.
---------
Co-authored-by: Markus Hartung <mail@hartmark.se>
#10710: locateCommand() in cliRuntime.ts collapsed a genuine probe timeout
(runProcess's timedOut flag) into the same reason:"not_found" as a truly
absent binary, on both the where.exe and `command -v` branches. Give
timeouts a distinct "timeout" reason, keep trying remaining command
candidates in locateCommandCandidate instead of treating a timeout as
terminal, and extend the settings-file fallback (cliInstallFallback.ts) to
also cover the new "timeout" reason, matching the scenario it already
existed for.
#10711: the Hermes Agent dashboard "Apply" flow only ever sends `keyId`
(never a raw `apiKey`), but the hermes-agent-settings POST handler never
resolved it, so generateHermesAgentConfig() always fell through to the
literal placeholder "YOUR_OMNIROUTE_API_KEY_HERE" for
providers.omniroute.api_key, delegation.api_key, and every
auxiliary.*.api_key. Resolve keyId server-side via getApiKeyById(), the
same precedented pattern already used by claude-settings/route.ts and
codex-settings/route.ts.
Bug 2 from #10710 (hermes tool-detector configPath) was already fixed by
commit 0a74bfbdea -- confirmed still intact,
no action needed.
Co-authored-by: Markus Hartung <mail@hartmark.se>
The guard shipped with #10695 watched two hand-picked modules. It now walks the static
import graph from all 753 "use client" files in src/ (plus the two originally pinned
entries), so the invariant is verified across the repo instead of where someone
remembered to look. Full sweep runs in ~750ms.
Two exclusions make that practical:
- `import type` is not an edge — TypeScript erases it before the bundler sees it.
Counting type imports turns 3 real findings into 29; a guard that cries wolf gets
switched off.
- Dynamic `import()` is still not followed. It does not break a bundle edge (that was
tried for #10692 and failed) but it does move the module into a chunk the browser
fetches on demand, which is a legitimate boundary.
The widened sweep immediately found what the narrow one could not: five value-form
imports of `db/batches` / `db/files` across three files under dashboard/batch, each
reaching db/core → the SQLite driver. All five bind only interfaces (BatchRecord,
FileRecord) used in type position, so the compiler was eliding them and the build stayed
green — the same latent shape as #10692 before #10647 removed the toolchain's tolerance.
Marking them `import type` makes the elision explicit instead of incidental.
Refs #10692
Co-authored-by: Xiangzhe <bakryun0718@proton.me>
* feat(compression): target-wire OmniGlyph stage and transport fidelity gate
Roda o OmniGlyph depois da tradução para o wire real do provedor, em vez do
corpo de origem. Um cliente OpenAI roteado para Claude deixava de comprimir com
skip:source_format_not_claude porque o corpo ainda estava em formato OpenAI
quando a engine era avaliada.
- dispatch nativo por wire: Anthropic Messages, OpenAI Chat Completions e
OpenAI Responses (input[] preservado, sem achatar para messages[]);
- estágio target-wire pós-translateRequest, com guarda contra dupla compressão
no caminho Claude→OpenAI;
- preserveSystemPrompt do OmniRoute mapeado para compressSystem: false;
- imageTransportPolicy: fidelidade de bytes/dimensões separada de supportsVision;
só Anthropic/Claude tem recibo byte-preserving, o resto é fail-closed;
- contagem de tokens de data URL PNG no wire OpenAI (marcador ;base64,);
- README e i18n en/pt-BR com claims escopados ao caminho medido.
* feat(compression): adota omniglyph 1.4.0 e tira o gate de modelo da env do host
O 1.4.0 introduziu escopos de segurança e passou a resolvê-los dentro de
isOmniGlyphSupportedModel() lendo process.env.OMNIGLYPH_PROFILE. Somado ao
OMNIGLYPH_MODELS que já existia, duas variáveis do ambiente do host decidiam em
silêncio o gate de TODO request do OmniRoute: passthrough desligaria a engine
inteira e OMNIGLYPH_MODELS admitiria modelos sem recibo medido, enquanto a UI
segue prometendo "Claude Fable 5 na rota direta medida".
O adapter passa a usar isOmniGlyphSupportedModelForScope() com escopo explícito
e fixa o escopo mais restrito como teto: a env só pode ESTREITAR a allowlist,
nunca alargar. Os dois wires compartilham a mesma lista no pacote desde o
1.4.0, então uma checagem cobre Anthropic e GPT.
- omniglyph ^1.3.1 -> ^1.4.0 (lock em 1.4.0);
- testes de regressão para os dois caminhos de sequestro por env;
- teste de contrato dos exports novos (escopo, perfis, accounting).
O 1.4.0 também traz, sem mudança de código aqui: correção do glyph K que era
lido como H, remoção do backtracking polinomial no secret-guard, overrides do
pnpm em pnpm-workspace.yaml e as transitivas vulneráveis resolvidas.
* feat(compression): expõe os perfis semânticos do omniglyph nos três wires
O 1.4.0 trouxe perfis nomeados (coding-safe, balanced, aggressive,
passthrough), mas só transformAnthropicMessages() os resolve sozinho: os
transformadores OpenAI recebem TransformOptions cru e ignorariam o campo. Um
perfil escolhido pelo operador valeria no wire Claude e sumiria no OpenAI. O
adapter passa a mesclar o perfil com mergeCompressionProfileOptions() antes de
chamar Chat Completions e Responses.
O default segue aggressive — a política que os recibos publicados mediram.
Medido nesta base: com coding-safe/balanced, uma sessão sem histórico acumulado
para em below_min_chars e a engine não faz nada, porque os dois fixam
minCompressChars no máximo e desligam system/tools/tool-results. Como a engine é
opt-in, um default assim entregaria "ligado, 0% de ganho".
O perfil é TETO, não piso: mergeCompressionProfileOptions não deixa um override
do chamador reabrir uma lane lossy que o perfil fechou. Coberto por teste, por
ser contra-intuitivo.
Também fecha um caminho em que o OmniRoute violaria a própria política: o wire
OpenAI do pacote não tem compressSystem — honra apenas compressTools,
gptHistory, minCompressChars e reflow, e sempre troca a instrução por um
ponteiro para a imagem. Com preserveSystemPrompt ligado, imagear assim queimaria
o prefixo quente que a decisão cache-aware está protegendo, sem nada no corpo
devolvido denunciando. A engine agora pula com
skip:system_preservation_unsupported_on_wire.
* feat(compression): contabilidade física do omniglyph com grau de evidência
O adapter descartava o TransformInfo inteiro, então a UI mostrava um número de
economia sem dizer de onde ele vinha — contagem do provider, estimativa ou só
diferença de bytes. O 1.4.0 expõe normalizeAccounting(), que classifica essa
evidência e resolve a semântica de cache por família: Anthropic reporta input,
cache-create e cache-read em buckets DISJUNTOS, enquanto OpenAI e xAI reportam
cached como SUBCONJUNTO do input. Somar à mão dá double-count silencioso.
O novo omniglyphTelemetry.ts não filtra por denylist — MONTA um objeto novo,
campo a campo, só com número e enum. TransformInfo mistura contadores
inofensivos com material que não pode ser persistido: bytes PNG,
imageSourceText(s), recoverable[].text, os sha8 de system/CLAUDE.md/primeira
mensagem, nomes de tags observadas e o bloco env (cwd, branch, versões). Copiar
o objeto inteiro transformaria telemetria de compressão em vazamento de prompt.
O teste de negação prova que segredo, caminho do operador, texto do system e
base64 não aparecem, e varre a allowlist exigindo que toda string seja de um
enum conhecido.
- provider threaded do chatCore e do bridge Codex WS até a engine; ausente vira
`unknown`, que faz o upstream recusar adivinhar buckets de cache;
- contabilidade propagada para o engineBreakdown do passo (o agregado do
pipeline soma todas as engines e não serviria);
- skip não emite contabilidade: zeros ali seriam indistinguíveis de "a engine
nem rodou".
* feat(compression): perfil do omniglyph configurável, persistido e documentado
Fecha o caminho do operador: o perfil já existia no adapter, mas só como
default de código. Agora atravessa schema Zod, normalizador do banco, API de
settings e a página dedicada do engine.
- OmniglyphConfig tipado + omniglyphConfigSchema (z.enum dos quatro perfis);
- normalizeOmniglyphConfig: nome desconhecido vindo do storage cai para o
default em vez de virar "roda com a política padrão";
- seletor na página do engine, com PATCH próprio — o perfil vive fora do mapa
`engines`, e mandá-lo junto reescreveria o mapa inteiro (o store persiste o
mapa como uma linha JSON só);
- i18n en/pt-BR descrevendo o custo medido de cada perfil, não só o nome;
- README e COMPRESSION_ENGINES.md com a regra do teto e o motivo de o default
não ser o perfil mais seguro.
Corrige de passagem um teste-irmão que ninguém via: o gate de transporte na UI
deixou de dizer "direct Anthropic" quando os wires OpenAI nativos entraram, mas
tests/unit/ui/omniglyphContextPage.test.tsx continuou afirmando a cópia antiga.
O arquivo inteiro estava excluído do vitest.config.ts como "#8618 pre-existing
failure", então a quebra passou silenciosa. Com a asserção alinhada o arquivo
fecha 3/3, e a exclusão sai — o próprio comentário mandava removê-la quando
corrigida.
A doc não nomeia OMNIGLYPH_MODELS: o gate de docs fabricadas está certo em
apontar que o OmniRoute nunca lê essa env — quem lê é o pacote.
* fix(i18n): paridade do locale vi com as chaves novas do perfil do omniglyph
`tests/unit/i18n-vi-completeness.test.ts` exige paridade ESTRITA de chaves entre
en e vi — diferente do ratchet `i18n:check-ui-coverage`, que passa com 80%. As 11
chaves do seletor de perfil entraram só em en e pt-BR, e o gate de cobertura
seguiu verde, então a quebra só apareceu na matriz completa do CI.
---------
Co-authored-by: Xiangzhe <bakryun0718@proton.me>
Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
* fix(deps): bump nanoid, dompurify for 2 new Dependabot alerts (#189, #190)
Bumps: nanoid ^3.3.17 (was transitive, now overridden), dompurify ^3.4.13
(with monaco-editor scoped override). Closes Dependabot #189, #190.
Remaining #182-#188 (js-yaml + mermaid) already closed by #9651 merge —
awaiting Dependabot re-scan.
npm audit → 0 vulnerabilities.
* fix(repo): harden .gitignore to also ignore a _tasks symlink (/_tasks)
_tasks is a SEPARATE nested git repo (gitignored). The pattern _tasks/ (trailing
slash) ignores only a directory, not a SYMLINK named _tasks. A self-referential
_tasks symlink can slip in via git add -A and, once pulled, checkout materializes
it over the real _tasks repo (destroying plans/specs/hands-off). Anchored /_tasks
ignores the symlink too, preventing re-capture.
* Hide health-check excluded models from /v1/models catalog (#10026)
Mirror the request-time exclusion rule (provider_specific_data.excludedModels)
in the unified catalog builder: a model is hidden when its provider has
connections but none of them is eligible for it. Applied across the
PROVIDER_MODELS, synced, custom, alias-backed, and managed-fallback loops
so ghost models no longer appear as available.
Co-authored-by: ritheshcn25 <ritheshcn25@users.noreply.github.com>
* fix(models): memoize getModelsDevPricing (event loop / healthz) (#10055)
* fix(models): memoize getModelsDevPricing for /v1/models catalog
resolveCatalogPricing called getModelsDevPricing once per model while
building GET /v1/models. Each call re-scanned models_dev_pricing and
JSON.parsed every row (~10k SQL scans + multi-GB parse work), pegging
the event loop so even /healthz timed out (#9685, #10052).
Memoize the parsed map until saveModelsDevPricing / clearModelsDevPricing
and add a unit test for invalidation.
Signed-off-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
* fix(db): invalidate modelsDevPricing cache on DB reset (#10055)
Copilot review fixes:
1. Register invalidateModelsDevPricingCache() with DB state reset system
so resetDbInstance() clears the process-local memo, preventing stale
pricing data from surviving across DB reset/restore operations.
2. Add test assertion verifying DB reset bypasses the memo (Copilot #10055).
The process-local memo at modelsDevSync.ts:204 caches getModelsDevPricing()
results until saveModelsDevPricing()/clearModelsDevPricing() to avoid
re-scanning all pricing rows on every /v1/models request. Without this hook,
backup restore and test DB resets would serve stale cached data from the
previous connection.
Tests: npm run test:unit:serial -- tests/unit/modelsDevSync-extended.test.ts
---------
Signed-off-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
* fix(oauth): route Zed hosted sign-in callback back to the dashboard port
Zed's native-app sign-in always redirects the browser to the loopback port
sent as native_app_port (hardcoded default 58443), where nothing listens:
the browser shows "site can't be reached" and the login looks broken even
though the token is in the URL. The manual paste fallback was broken too -
handleManualSubmit requires a ?code= param that Zed's callback
(user_id + access_token) never carries, so the flow could never complete.
- zed-hosted: derive native_app_port from the dashboard's own loopback
port so the redirect lands back on OmniRoute; remote/LAN origins keep
the old default port and the paste flow
- app root: forward ?user_id=...&access_token=... to the /callback relay
instead of dropping the query string on the /dashboard redirect
- /callback relay: recognize the Zed payload (no code param) and relay the
full URL as the exchange payload; allow postMessage to both loopback
spellings (localhost/127.0.0.1) of the same port
- OAuthModal: zed-hosted popup auto-completes on true localhost; the
manual paste path passes the full URL through to the exchange instead
of erroring with "No authorization code found"
- manual input panel: zed-hosted-specific placeholder and hint
- tests: extend the postMessage scope guard with the loopback same-port
trusted origins
* changelog: fragment for #10517
* fix(oauth): derive Zed native_app_port from server config, not browser scheme/port
resolveDashboardLoopbackPort() previously re-derived the dashboard's loopback
port from the browser-supplied redirectUri (window.location.port ||
protocol === "https:" ? "443" : "80"), which produced http://127.0.0.1:443/
native-app redirects when the dashboard was reached over HTTPS on its
implicit default port (e.g. behind a local TLS-terminating reverse proxy) -
a scheme/port mismatch, since Zed's own redirect is always plain http and
nothing serves plain HTTP on 443 in that scenario.
This code runs server-side (in the OAuth authorize API route), so once the
redirect URI's hostname is confirmed loopback it now uses the OmniRoute
process's own authoritative listening port via getRuntimePorts()
(OMNIROUTE_PORT/PORT/DASHBOARD_PORT) instead of re-deriving it from the
browser-observed scheme/port. Non-loopback (remote/LAN) redirect URIs still
return null and fall back to the manual paste flow.
Adds tests/unit/zed-hosted-loopback-port-derivation.test.ts (8 cases)
covering the port-derivation logic directly, including the HTTPS-default-port
mismatch scenario that motivated this fix, env-var precedence, IPv6 loopback,
non-loopback/remote fallback, and buildAuthUrl's native_app_port wiring.
Also rebaselines config/quality/file-size-baseline.json for OAuthModal.tsx's
own growth from this PR's earlier commit (1134->1149 gate units) - legitimate
zed-hosted callback wiring at the existing provider-switch chokepoint, not
extractable without a broader modal decomposition (tracked in #3501).
The live Zed OAuth handshake itself (root -> /callback -> OAuthModal exchange
against the real zed.dev endpoint) still needs a documented VPS smoke test
per Hard Rule #18; this fix covers the TDD-able port-derivation logic that
motivated the change.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Signed-off-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@outlook.com>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
Co-authored-by: ritheshcn25 <rithesh.chandran@snb.ca>
Co-authored-by: ritheshcn25 <ritheshcn25@users.noreply.github.com>
Co-authored-by: Ravi Tharuma <25951435+RaviTharuma@users.noreply.github.com>
Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* feat(responses): virtualize previous_response_id continuation regardless of upstream support
OmniRoute now exposes OpenAI-compatible previous_response_id/store
continuation to clients unconditionally, even when the selected upstream
provider has no native Responses-API state support. Reconstruction happens
server-side in handleChatImplementation, before any downstream validation
or provider translation: OmniRoute resolves the response id back to the
full input/output it previously produced, prepends it to the client's
delta, and forwards the full reconstructed history upstream exactly as it
does today. Client<->OmniRoute traffic shrinks to the new delta only;
OmniRoute<->provider traffic is unchanged.
Storage reuses the existing call-log pipeline artifact (already gated by
call_log_pipeline_enabled, already retained/cleaned up by the existing
call-log lifecycle) instead of duplicating conversation content into a
second store -- only a lightweight call_logs.response_id index is new.
Every lookup is scoped by api_key_id so one client can never resolve
another client's stored conversation, and any unresolvable/missing/
size-limit-omitted state fails closed with OpenAI's own
previous_response_not_found contract.
Stacked on feat/openai-responses-store-toggle (#10121).
* feat(dashboard): agentic conversation tracking with live transcript view
Every agentic chat request now gets a conversation id (X-ConversationId
response header). OmniRoute detects when a follow-up request continues the
same conversation via fingerprint + bounded prefix-hash matching, with a
strict-growth invariant to prevent false merges between independent
single-shot requests that happen to share identical opening content.
Continuation detection excludes the system message from the identity
anchor, since real coding-agent CLIs commonly regenerate it every request
with live context (timestamp, cwd, git status) — without this, that
volatility alone broke every continuation check against real traffic.
- `/dashboard/logs`: new toggleable Conversation column.
- `/dashboard/logs/timeline`: requests sharing a conversation id share a
timeline lane, connected by an arrow, with a configurable lane-reuse
window.
- Request detail panel: new Full Conversation transcript above the raw SSE
event stream — Markdown rendering, per-turn timestamps, turn-relative
view, click-any-turn navigation, live auto-refresh building the
transcript in real time from the in-flight SSE chunk buffer while a
request is still streaming, auto-scroll-to-bottom as the live turn grows.
- New `/dashboard/conversations` page listing conversations with 2+ turns,
no-forking model (an edited/duplicated mid-history turn mints its own
independent conversation instead of merging), pagination, duplicate-
anchor fix.
- Configurable auto-refresh intervals on both the timeline and
conversations list pages.
- Responses API tool-call gap fix: turnsFromOpenAiMessages only handled
role-based Chat Completions messages, so bare {type:"function_call"} /
{type:"function_call_output"} / {type:"reasoning"} items (real Responses
API traffic) silently vanished from the Conversation Context panel.
- truncateForLog now counts input[] (Responses API), not just messages[]
(Chat Completions), so a truncated /v1/responses request still shows a
placeholder instead of nothing.
- RequestTimeline.tsx now reads the same debugEnabled/emailsVisible
settings RequestLoggerV2.tsx already used, instead of hardcoding both
false — the timeline view never showed SSE/stream-chunk events or
respected email-masking, regardless of the actual setting.
Migrations 147/148 (agentic_conversations, conversation_turn_nodes) — 135
and 136 are now taken upstream; 143-145 are documented KNOWN_GAPS, so this
uses the next free slot past upstream's current highest.
Test plan:
- npm run typecheck:core — clean
- npm run lint — clean
- node --import tsx/esm scripts/check/check-migration-numbering.mjs — OK, 0 collisions
- 109 unit tests across the conversation-tracking, migration-renumber, and
dashboard-wiring surface — 0 failures
* refactor(dashboard): reuse call-log artifacts for conversation transcript content
conversation_turn_nodes no longer stores turn text/tool-call content
(text_preview/block_kind/tool_name) -- it's identity-only now (id/parent/
content_hash), matching agentic_conversations' existing lightweight-index
shape. Every node's originating request is already fully captured by the
call-log pipeline artifact its last_correlation_id points at, so the
/dashboard/conversations tree view resolves each node's actual display
content on demand from there (open-sse/services/conversationTurnContent.ts),
re-running the same extractCanonicalTurns/hashTurnContent the write path
used and matching by content_hash, instead of duplicating conversation
content into a second store under a separate retention/gating policy. This
also drops the old 8000-char text_preview truncation entirely -- resolved
content is always full and untruncated.
The frontend contract is unchanged (tree API still returns
{textPreview, blockKind, toolName} per node), so the dashboard UI itself
(page.tsx, RequestLoggerDetail/RequestTimeline, sidebar, i18n) needed no
changes.
Renumbered the cherry-picked 147/148 migrations to 153/154 -- 147 now
collides with 147_api_keys_model_access_mode.sql, which landed on
release/v3.8.50 after this work was originally built.
Also includes a standalone, unrelated fix carried along from this rebase:
close isProviderModelHidden's missing function-body brace in
modelSelectModalHelpers.ts (separately landed as #10206).
Stacked on feat/responses-previous-response-id-virtualization (#3), which
is itself stacked on feat/openai-responses-store-toggle (#10121).
* fix(dashboard): resync conversation list on open so the live-text poll starts immediately
openConversation() seeded activeConversation (and therefore activeCallLogId,
which gates the live-partial-text poll effect) from whatever row snapshot the
list's own fixed-interval poll last produced. A conversation opened right
after a reply started streaming -- after that tick, before the next -- had
activeCallLogId still null, so the live-text poll never started; only a
subsequent background list-poll resync (already existed) picked it up,
which is why closing and reopening the same conversation "just worked".
loadConversations() is now a shared callback so openConversation can force
one immediately on open instead of waiting on pollSeconds.
Live-verified against omniroute-dev: opening a conversation mid-stream now
shows live reasoning on the first open.
* style: prettier formatting for conversationTurnContent.test.ts
* fix(db): close migration numbering gap left by decoupling from #3/#10262
153/154 (originally 154/155) were chosen back when this branch stacked on
top of the previous_response_id migration (153_call_logs_response_id.sql).
Decoupling removed that migration from this branch's history, leaving an
unused 153 slot that check-migration-numbering.test.ts correctly flags as
a gap.
* refactor(dashboard): split RequestTimeline/RequestLoggerDetail under the 1000-line file-size cap
Both files exceeded check-file-size's new-file cap after this PR's own
additions (RequestTimeline 1048, RequestLoggerDetail 1163). Extracted pure
non-component logic (types, constants, allocateLanes and its helpers) out
of RequestTimeline.tsx into RequestTimeline.utils.ts, and the two
self-contained presentational sub-components (PayloadSection,
ConversationContextSection + its private helper) out of
RequestLoggerDetail.tsx into RequestLoggerDetail.sections.tsx. No behavior
change; existing external imports (default exports, allocateLanes,
TimelineLog, CONVERSATION_LANE_REUSE_STORAGE_KEY) still resolve from the
original file paths.
* fix(db): renumber agentic-conversation migrations to clear 153 collision + sync migration-count docs
The refresh-merge of release/v3.8.50 exposed that the feature's three
migrations collided at slot 153 with the base's radar_local_model_state
(153) and its own call_logs_response_id. Migration runner enforces unique
numeric prefixes -> every DB init threw, red-ing Vitest, all Unit shards and
the DB-backed quality gates. Renumber the feature's pair to
155_agentic_conversations / 156_conversation_turn_nodes and move
call_logs_response_id to 154 (keeps 153_radar base-owned, preserves
agentic-before-turn_nodes ordering). Update SQL headers and the
154/156 references in feature code + tests.
Migration count is now 151 (was 148 stale in README/AGENTS/llm.txt) — sync
the doc counts to clear the docs-accuracy gate.
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
* fix(ui): drop unused CONVERSATION_LANE_REUSE_STORAGE_KEY re-export from RequestTimeline
Knip 6.32 (baseline 415) flags the public re-export of
CONVERSATION_LANE_REUSE_STORAGE_KEY from RequestTimeline.tsx as dead: no
external consumer imports it through that re-export (it is imported and
used directly from RequestTimeline.utils.ts inside the component). Removed
the unused re-export; the internal import stays. DEAD_TOTAL 416 -> 415,
back to the frozen baseline.
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
* fix(agentic-conversations): guard resolveConversationId, drop dead whole-chain export
- Wrap resolveConversationId() in try/catch in chat.ts, matching the
defensive pattern used by every other best-effort side call nearby, so a
DB hiccup in conversation tracking can't turn a working chat request into
a hard failure.
- Remove getConversationTurnTree: knip's project scope excludes tests/**,
so an export used only by tests can never register as used there. Swap
its 8 test call sites to the paginated getConversationTurnPage (already
the dashboard's canonical query) with a generous limit, collapsing to one
query path instead of keeping a second whole-chain export alive solely
for test convenience.
- Regenerate i18n llm.txt mirrors from root (pre-existing drift on this
branch, unrelated to the above, caught by the docs-sync pre-commit gate).
Addresses PR review feedback.
* fix(i18n): close requestLogger conversation-column gap, fix domain-modules count drift
- fr.json, vi.json were missing requestLogger.columns.conversation (added
in the conversation-tracking feature), failing i18n-vi-completeness.test.ts.
- docs/i18n/*/llm.txt mirrors still said 117 domain-specific files after an
earlier rebase fixed the migration count but missed this companion number,
failing check-docs-sync.mjs across all 42 locales.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(docs): restore PROXY_LOG_INCLUDE_IPS env/doc entries (env-doc-sync red)
.env.example and docs/reference/ENVIRONMENT.md were both missing the
PROXY_LOG_INCLUDE_IPS entry that src/lib/proxyLogger.ts already reads
(confirmed present at this branch's merge-base too, so this predates
the conversation-tracking work and is unrelated to it) -- the entry
was added on release/v3.8.50 after this branch's last sync and this
branch never picked it up. That gap red-lines
tests/unit/check-env-doc-sync.test.ts and
tests/unit/issue-7793-env-doc-sync-repro.test.ts (Unit Tests
fast-path 2/4 in CI). Restore both entries verbatim from the current
release/v3.8.50 tip -- no feature-code change.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: hartmark <hartmark@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* feat(api): add provider quota telemetry, adaptive routing, and status inventory
Adds a read-only OmniRoute status/inventory surface plus supporting
resilience and usage-tracking infrastructure:
- src/lib/quota/providerQuotaTelemetry.ts, providerCapabilities.ts:
provider quota state and capability signals, sourced from configured
metadata rather than invented values; unknown stays unknown.
- src/lib/resilience/adaptiveCircuit.ts, failureClassification.ts:
circuit state with lazy recovery and explicit failure classification.
- src/lib/usage/usageLedger.ts, budgetGuard.ts, modelPricingRegistry.ts:
internal usage tracking and budget allow/warn/deny decisions, kept
separate from upstream-reported quota (never conflated).
- src/lib/routing/adaptiveRouting.ts: excludes exhausted-quota and
open-circuit candidates from routing, penalizes approaching-limit.
- src/lib/omnirouteStatus.ts + src/app/api/omniroute/status,
route/preview: read-only status endpoint; never issues a live
upstream model request (asserted via liveRequestExecuted: false).
- src/lib/db/quotaPools.ts: adds ensurePool() for idempotent pool
management by automation/CLI callers, following the existing
group-demo default-group convention.
- scripts/omniroute-verify.mjs (+ omniroute:verify script): local
verification against the running gateway.
9 new unit tests, all passing. typecheck:core clean relative to base
(release/v3.8.50) -- the 2 pre-existing gateways.ts errors are tracked
separately in #9985 and untouched by this change.
* test(cli): align cli-machine-token assertions with HMAC-SHA256 64-char format
The quota-telemetry feature hardens cliToken to HMAC-SHA256(machineId, SALT)
(64-char hex, pristine machine id). Update the regression test to the new
format and mirror the production derivation in the different-machine-id check.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
Co-authored-by: desamours-hub <desamours-hub@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* feat(admission): per-target lane-aware probes for combo/fusion fan-out (#9654 Wave 2)
Combo and fusion fan out N targets without ever consulting the adaptive-admission
layer: the parent request holds one lease, but each fan-out target is dispatched
unconditionally. With virtual lanes enabled (OMNIROUTE_CHAT_VIRTUAL_LANES=1), a
connection whose lane queue is full now SKIPS additional fan-out targets instead
of piling more queued work onto an already-congested session.
Adds PerTargetAdmissionHook (admission/types.ts) + createPerTargetAdmissionHook
factory (chatAdmission.ts): strictly non-blocking (maxWaitMs 0 - skip, never
queue), a no-op when virtual lanes are off, keyed to the parent tenantKey, and
release-on-admit so the probe is a capacity gate, not a hold.
Threaded through every parallel fan-out path:
- priority/weighted executeTarget + round-robin skip chains (combo.ts)
- fusion panel before fan-out (fusion.ts), judge fallback prefers survivors
- chaos parallel panel (autoCombo/chaosEngine.ts)
- tryFusionDispatch / tryRuntimeUnitDispatch / buildBaseOptions (dispatchPrelude.ts)
- chat.ts primary + safety-net redirect call sites
Snapshot exposes virtualLanes so the no-op gate is cheap and honest.
Tests: tests/unit/combo-lane-awareness-9654.test.ts (10 tests) - factory
semantics, priority/RR skip, fusion panel drop + all-skipped 503, no-hook
backward-compat baseline.
* feat(flags): activation UX - env-wins adaptive virtual-lanes flag + env docs (#9654 Wave 2)
U7: make adaptive virtual admission lanes discoverable + activatable.
- New OMNIROUTE_CHAT_VIRTUAL_LANES feature flag (boolean/runtime/requiresRestart) in featureFlagDefinitions + en.json i18n key.
- lib/admissionVirtualLanes.ts: env-wins resolver (env > DB > default) + boot warm folding a DB-sourced override into the process-global runtime env via reloadAdaptiveAdmissionRuntime(options.env) - no process.env mutation, no open-sse changes. Env still wins; DB toggle gates at next boot.
- GET /api/settings/feature-flags special-cases the flag to report the gate true source (ccDiscoveryAliases precedent); flagPayload helper dedupes the payload shape.
- Wire the warm into instrumentation-node registerNodejs (non-fatal, DB-ready).
- Document the master switch in .env.example + ENVIRONMENT.md with the system-1/system-2 distinction; zero new env-doc-sync drift.
- 11 new tests (resolver precedence + warm); 60/60 across feature-flag suites; typecheck core clean; ESLint + doc gates green.
* feat(mcp): surface adaptive admission lane data in omniroute_get_health (#9654 Wave 2)
U8: make adaptive virtual-lane admission visible to agents via the MCP health tool. handleGetHealth now surfaces a curated adaptiveAdmission block from the health payload (which already carried the runtime snapshot but was dropping it): virtualLanes/pressure/utilization/laneCount/laneQueuedCount/laneQueuedCost, laneTenants capped at top-10 by queued cost, admitted/rejected/wouldReject counts, shutdown. Block omitted entirely when the health endpoint reports none.
isLaneFlagOn mirrors the runtime 1|true convention so a string serialization can never invert a boolean lane report. getHealthOutput schema extended with the matching optional shape; tool description updated.
4 new dispatch tests (full block, top-10 cap/order, omission, defensive coercion of string flags + malformed lane entries) - 22/22 in essentialTools.test.ts. README: Adaptive Admission Lane Data table + Skills & Tool Navigability audit (29/43 schema entries covered, 14 undocumented, tool_search keyword runtime discovery, full catalog in docs/frameworks/MCP-SERVER.md).
No new lint errors (4 pre-existing in server.ts), typecheck core clean, doc counts + fabricated-docs gates green.
* docs: add changelog entry for #9654 Wave 2 (#10039)
* fix(codeql): suppress js/insufficient-password-hash false positive in lane-key fingerprinting (#10039)
resolveSessionId sha256-hashes bearer/x-api-key/x-goog-api-key to derive a deterministic, non-reversible per-key lane-bucket ID for virtual admission lanes (#9654). This is not password storage or verification, so the rule is a false positive; suppress it inline (same house style as src/lib/sync/tokens.ts) to clear the codeqlAlerts ratchet (2 > baseline 1) that blocks #10039 and every PR against release/v3.8.50.
* docs(mcp): complete MCP server README tool reference (#10039)
The MCP server README covered only 29 of the 43 schema entries, listing the
remaining tools solely as a gap note with omniroute_tool_search as the runtime
fallback. Add tool-reference tables for the agent-skills trio, oneproxy trio,
web_fetch/web_search, tool_search, create_combo, set_routing_strategy,
pick_fastest_model, sync_pricing, and db_health_check so the README covers the
full schemas catalog, and fold the coverage note into the tool_search discovery
paragraph.
* fix(chat): drop unused correlationId from safety-net combo redirect (#10039)
handleComboChat's HandleComboChatOptions has no correlationId member and
the combo pipeline never consumes it; the property was copied from the
handleSingleModelChat options shape by accident and introduced a new
TS2353 under the open-sse workspace typecheck gate.
* fix(i18n): translate featureFlagChatVirtualLanesEnabledDescription into 42 locales (#10039)
en.json gained the flag description in this PR but the locale catalogs
were never mirrored, failing the pt-BR key-parity (#6695) and vi
completeness gates. Adds a real translation to every locale, keeping the
zh-CN/zh-TW glossary canonical terms (提供者/儀表板) and no ICU drift.
* chore(quality): ratchet open-sse-typecheck baseline down (#10039)
The Wave 2 admission refactor removed 66 baselined open-sse type errors;
re-freeze the baseline so the gate pins the new, tighter state.
* docs: resync provider reference to 341 and CLI tools to 34
The release branch gained an 11th no-auth provider (freeaiapikey registry
resync, #10233) and a 26th CLI Code tool without regenerating the
auto-generated docs, leaving every PR against release/v3.8.50 failing the
Docs Gates strict validator (code 341 vs doc 340, CLI 34 vs "33 tools").
Regenerate docs/reference/PROVIDER_REFERENCE.md and sync the provider/tool
counts across README.md, AGENTS.md, llm.txt plus 42 i18n mirrors,
package.json description, and the four diagram SVGs.
* fix(tests): align count expectations with live catalogs (pre-existing release drift)
Release/v3.8.50 currently fails five gates on its own tree; this PR inherits
them. Fix the stale expectations to match live code:
- feature-flags-settings: 48 -> 49 flags (Wave 2 adds OMNIROUTE_CHAT_VIRTUAL_LANES)
- cli-tools-schema / cli-catalog-counts: 33 -> 34 tools (zcode added; 26 code = 21 visible + 5 none)
- optional-transformers-dependency: onnxruntime-node ~1.24.3 -> ~1.27.0 (bump #10382)
- stryker.conf.json: register chatcore-header-drop-warn-dedupe-10315 test
- check-public-creds: freeze zcodeProtocol clientId false positive (client identifier, not a credential)
* fix(tests): follow release's onnxruntime-node revert to ~1.24.3
release/v3.8.50's #10543 pinned onnxruntime-node back to ~1.24.3 after
#10403's ~1.27.0 bump caused npm to nest a second native copy under
@huggingface/transformers and broke the Docker SONAME contract. This
PR's own drift-alignment commit (57b9c033) predates that revert and
still expected ~1.27.0; the 3-way merge did not flag it as a textual
conflict since only one side touched this exact line, but the merged
tree became internally inconsistent (package.json ~1.24.3 vs test
expecting ~1.27.0). Align the test with the now-canonical release
value.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(quality): dedupe stryker.conf.json chatcore-header-drop-warn-dedupe entry
The 3-way merge applied both sides' insertion of the same test-file entry
at different positions, producing a duplicate with broken indentation.
Adopted release's clean version of the file.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: Brandon Bennett <brandonbennett@macbookair.myfiosgateway.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Brandon Bennett <branben@users.noreply.github.com>
* fix(providers): make upstream model sync opt-in and preserve manual overrides
(cherry picked from commit 0a84f5496896a95856e834112b3d813fa1b87d38)
* test(providers): cover upstream model sync controls
* fix(providers): fix pre-existing tests broken by opt-in model sync + sync i18n keys
The upstream model auto-fetch opt-in default flip made 3 pre-existing tests
short-circuit before reaching the paths they exercise, because their
connection fixtures never set providerSpecificData.autoFetchModels: true:
- tests/unit/provider-models-route-lan-guard.test.ts (#6939 SSRF-guard tests)
- tests/unit/openrouter-embeddings-catalog-6976.test.ts (live discovery merge/dedup)
- tests/unit/provider-models-route.test.ts (Kimi Coding auth-header test —
this was mislabeled as base/catalog drift during review, but is the same
root cause: without autoFetchModels the mocked fetch is never reached and
the route falls back to local catalog data instead)
Also syncs the 11 new providers.autoFetchModels*/overridesUpstreamModel*/
resetToUpstreamDefaults* i18n keys from en.json/zh-CN.json to the remaining
40 locale files via a narrowly-scoped ad-hoc translation script (only these
11 keys — leaves each locale's pre-existing, unrelated missing-key backlog
untouched).
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(providers): fix remaining pre-existing tests broken by opt-in model sync
Rebase surfaced that the 'Kimi Coding' CI failure flagged as possible base
drift during review was actually the same root cause as the lan-guard and
openrouter-embeddings fixes: 29 pre-existing tests in
tests/unit/provider-models-route.test.ts (of 59 total) short-circuit under
the new autoFetchModels opt-in default because their connection fixtures
never set providerSpecificData.autoFetchModels: true, so they never reach
the live-fetch/validation paths they were written to exercise (fetch mocks
never called, base-URL validation never reached, live models never merged).
Adds providerSpecificData.autoFetchModels: true to each affected fixture.
No production code or test assertions changed — same TEST-fixture-only
pattern as the lan-guard and openrouter-embeddings fixes. All 59 tests in
the file now pass (was 30/59).
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Diagnosed while chasing the reused-output-index incident (see
705ac7335 / OpenClaw issue #123342): every call-log artifact showed a
wire-clean response, even for requests that actually failed, because
withEarlyStreamKeepalive injects its startup/keepalive/error frames
directly into the outer response stream, entirely outside the request
handler's own reqLogger. reqLogger.appendConvertedChunk (which
populates pipeline.streamChunks.client) never sees those bytes — only
what chatCore.ts's own SSE writer produced. The persisted artifact was
answering "what did the handler generate," not "what did the client
actually receive," which is the wrong question when diagnosing a
client-visible stream defect.
withEarlyStreamKeepalive wraps the handler's Promise from OUTSIDE its
call tree; the reqLogger it needs to feed is created deep inside
chatCore.ts, after routing/model/provider resolution, and doesn't
exist yet when the keepalive frames are written. The two sides share
no reference — only an identifier, if one is deliberately threaded
through both.
Fix: responses/route.ts now generates a correlationId before calling
handleChat, passes it as handleChat's existing (already-supported,
previously-unused-here) 4th positional arg — which chatCore.ts already
threads into trackPendingRequest's metadata as entry.correlationId,
zero changes needed there — and also into
withEarlyStreamKeepalive's options. The wrapper buffers every direct-
to-client write (startup frame, periodic ticks, in-band error frames)
via the new earlyKeepaliveByteBuffer module, keyed by that same id.
chatCore/attemptLogging.ts, which already has correlationId in scope
right where it assembles the final pipeline payload before saveCallLog,
takes the buffered bytes and prepends them into streamChunks.client in
send order. The verbatim-forwarded real response body is deliberately
NOT re-recorded here — the handler's own reqLogger already captures
that; recording it twice would duplicate it in the artifact.
The buffer is consumed exactly once per correlationId and swept on a
10-minute TTL so a request that never reaches the persist call
(aborted, detailed logging disabled, a route that doesn't opt in)
cannot leak entries forever.
Scoped to /v1/responses only, where the incident actually happened.
/v1/chat/completions and /v1/messages call withEarlyStreamKeepalive the
same way and would need the identical two-line route change to opt in;
left as a follow-up rather than bundled in sight-unseen.
Test plan:
- tests/unit/early-keepalive-byte-buffer.test.ts (new): record/take
ordering, single-consumption, per-id isolation, empty-input no-ops,
unbounded-growth cap
- tests/unit/early-stream-keepalive.test.ts: two new tests — a
correlationId records the startup frame and keepalive ticks but NOT
the forwarded body; omitting correlationId is a true no-op
- tests/unit/attempt-logging-early-keepalive-merge.test.ts (new): real
temp-DB end-to-end proof against the actual persisted call-log row —
early bytes prepended in send order, consumed exactly once, no-op
without a correlationId, gated by detailedLoggingEnabled matching the
existing streamChunks capture gate
- tests/unit/chatcore-attempt-logging.test.ts (existing): unchanged,
still passing — confirms the merge addition doesn't disturb existing
persistence behavior
- 44 passed total across the above plus earlyStreamKeepalive.test.ts,
2 pre-existing skips unrelated to this change
- tsgo --noEmit: clean on all touched files
* feat(resilience): scope auto-disable banned accounts to subscriptions
Prepaid API keys should stay in the routing pool after a permanent-ban
signal; subscription/OAuth accounts can still be deactivated. Default
scope remains all so existing installs do not change.
* docs(security): document auto-disable scope and log skipped prepaid keys
Keep the operator ban-detection page aligned with the new setting and
reuse the shared scope enum in the settings schema and dashboard radios.
* chore(changelog): name the auto-disable scope fragment for #10617
* docs(settings): treat free login seats as auto-disable targets
The first-cut scope is still all vs login-style auth. Copy now states
that paid subscriptions and free accounts both disable, while prepaid
API keys stay in the pool until per-account overrides exist.
* i18n: backfill autoDisableBannedScope keys across all locales
npm run i18n:sync-ui — the 6 new autoDisableBannedScope* keys landed
in en.json and vi.json but not the other 40 locales (including
pt-BR), tripping the pt-BR no-drift regression test (#6695).
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(auth): add missing state parameter to OIDC authorization URL
The OIDC login route generates a state UUID and stores it in the
oidc_state cookie, but never includes it in the authorization URL.
This causes the OIDC callback to receive state=null, failing with
'oidc_error=missing_code' because the provider has no state to echo.
Add url.searchParams.set('state', state) after setting scope, so the
state parameter is sent to the OIDC provider and returned in the
callback for proper CSRF protection.
* test(auth): add regression coverage for OIDC login state parameter
Adds a TDD regression test proving the fix in this PR: the OIDC login
route now includes the state query parameter in the authorization
redirect URL, and it matches the oidc_state cookie value set on the
same response. Modeled on tests/unit/oidc-callback.test.ts.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(auth): use originEarly for OIDC err redirects (#10224)
* test(auth): verify OIDC err redirects use proxy origin (#10224)
---------
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(audio): fall back nested STT models when the prefix provider has no credentials
Bare ids such as deepgram/nova-3 prefix-match the native provider and 400
when that key is missing, even if OpenRouter lists the same model. Retry
the gateway and mention qualified catalog ids in the error.
Closes#10583
* test(audio): scope whisper-1 fallback test to a 2-provider registry
nanogpt was added to AUDIO_TRANSCRIPTION_PROVIDERS (already merged,
unrelated to this fix) with a bare "whisper-1" model id, which now
intercepts findAlternateAudioProvider's first candidate before the
qualified-alias branch this test exists to cover. Scope the test to a
local {openai, openrouter} registry subset so it deterministically
exercises the qualified `${provider}/${model}` fallback regardless of
future providers that also list a bare "whisper-1" id.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* feat(providers): complete Jina AI via OmniRoute including Omni multimodal
Dashboard and env keys share one Jina credential pool, native v5 Omni
{text}/{image}/{content} docs pass through /v1/embeddings intact, and
classify/segment/search are proxied without a third unused Jina card.
* chore(changelog): name Jina complete-provider fragment for #10581
* feat(providers): make Gemini Embedding 2 multimodal work via OmniRoute
Route gemini-embedding-2 through embedContent/batchEmbedContents so N
OpenAI input items become N vectors, pass through native multimodal
parts, and use dashboard Gemini keys (GEMINI_API_KEY only as fallback).
* fix(providers): resolve rebase fallout for Jina/Gemini embeddings
- narrow the two new no-explicit-any violations introduced by this PR
(validateJinaFoundationProvider's params + catch, search.ts's
normalizeJinaSearchResponse data param)
- cast credentials to Record<string, unknown> at the two quota-preflight
call sites in src/sse/services/auth.ts so the new JinaEnvCredentials /
GeminiEnvCredentials union members type-check without loosening the
allRateLimited narrowing used elsewhere in the same function
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(a2a): use a constant-time bearer compare in /api/a2a/tasks
* fix(a2a): drop new Function from tasks-auth test in favor of dynamic import
The regression test for the constant-time bearer compare loaded tokensMatch
and authenticateA2A by regex-extracting their source and eval'ing it via
new Function, which trips the repo's no-new-func/no-implied-eval ESLint
rules (error-level everywhere, including tests). Export both helpers as a
test seam from the route module (mirrors the existing
bridgeSecretMatches/authRouteInternals pattern) and import them directly
in the test instead. Also drops the now-unused eslint-disable directives.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* feat(providers): optional AI Horde API key and live image catalog
Allow a registered Horde key on the no-auth connection and send it for
chat and image jobs. List only image models that currently have workers,
and generate through Horde's native async API.
# Conflicts:
# open-sse/config/imageRegistry.ts
# src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx
# src/shared/constants/providers.ts
# src/sse/services/auth.ts
* fix(providers): validate AI Horde keys against find_user
The OpenAI-compatible /v1/models probe returns 200 for any Bearer token
on oai.aihorde.net, so Check always succeeded. Use Horde's /v2/find_user
lookup instead; an empty key still counts as the optional anonymous path.
* chore(changelog): name the AI Horde fragment for #10542
* fix(images): harden AI Horde optional-key selection and outbound fetches
- Optional-key selection now honors connection health (rate-limit cooldown
and terminal/unavailable test status) before handing a stored key back,
rotating to the next healthy key or falling back to the anonymous no-auth
path instead of using an unhealthy stored key.
- Route the Horde submit/check/status/cancel and catalog calls through the
repository's bounded outbound-fetch helper (timeout, no more bare fetch())
and route R2 image downloads through the established bounded remote-image
fetch (SSRF host guard, DNS-rebinding pin, streaming byte cap, redirect
limit) instead of an unbounded fetch().
- Extend the generation deadline to cover the full request lifecycle
(catalog freshness check, submit, polling, and image download), and add a
regression test proving that exceeding the deadline issues a DELETE
cancel to Horde's API rather than only timing out locally.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: pqr <pqr@soraka.ititti.es>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(providers): test token-backed web sessions
* fix(providers): restrict token-web-session test dispatch to validated providers
Narrow shouldUseApiKeyConnectionTest to the token-kind web-session providers
that actually have a token-aware connection validator (deepseek-web, kimi-web,
tinycms-web, copilot-m365-web, copilot-web, zai-web). WEB_SESSION_CREDENTIAL_REQUIREMENTS
marks more providers as kind: "token" than have a matching validator in
SPECIALTY_VALIDATORS (hailuo-web, microsoft-designer-web, t3-chat-web, promptql) — those
were falling through to the generic cookie-based validateWebCookieProvider probe, which
sends the stored credential as a Cookie header and treats most non-401/403 responses as
valid, so an invalid token could be reported as a healthy connection.
Add regression coverage for hailuo-web and promptql (plus microsoft-designer-web and
t3-chat-web) proving they stay off the API-key test path, and for every currently
validated token-kind provider proving they still use it.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(providers): fall back to public Code Suggestions endpoint on GitLab Duo direct_access 401 (#10365)
* fix(providers): extend GitLab Duo 401 fallback to the connection-test path (#10365)
The chat-completion path (open-sse/executors/gitlab.ts) already falls back to
the public Code Suggestions completions endpoint when the direct_access
exchange is rejected with 401, but testOAuthConnection() / the dashboard
Retest button still reported the connection unhealthy on the same 401 —
even though a real chat request through that connection would have
succeeded via the fallback. Apply the identical fallback contract to the
connection-test path (first attempt and the post-refresh retry), sharing the
predicate with the executor via shouldFallbackToPublicCodeSuggestions.
---------
Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
* fix(memory): auto-check Qdrant health on mount and stop false-red badge
The Qdrant engine card on /dashboard/memory?tab=engine showed a red
"Error" badge after every page refresh even when Qdrant was healthy:
the badge derives its state from a health check, but the mount effect
only fetched settings + embedding models — health started as null and
the render treated `health?.ok` (undefined) as a failure. Clicking
"Test connection" (which runs the same server-side /readyz check)
immediately turned it green, proving the connection was fine.
Two changes:
- Auto-run the health check on mount once settings load and Qdrant is
enabled, so a refreshed page reflects the real state (verified live:
/api/settings/qdrant/health returns ok:true in ~2ms on a healthy
compose deployment).
- While health has not been checked yet (null), render a neutral gray
"Testing..." state instead of red — red is now reserved for an
actual failed health check.
Regression test added (fails on the old code): with enabled settings
and a healthy mock, the card must hit /api/settings/qdrant/health on
mount and show statusActive, never statusError.
* chore(changelog): fragment for #10489
* Merge branch 'release/v3.8.50' into fix/qdrant-health-badge
* test(fix): refresh expired alibaba quota sample validity and onnxruntime pin for v3.8.50 base
- alibaba-free-tier-quota-fetcher.test.ts: sample quotaValidityPeriod
(2026-08-16 16:00 UTC) is in the past, making every quota entry classify
as expired/not_capable; bump to 2028-01-01 UTC so the text/merge
classification tests exercise the intended path again.
- optional-transformers-dependency.test.ts: onnxruntime-node pin assertion
updated from ~1.24.3 to ~1.27.0 to match package.json (bumped by #10403);
the regular-not-optional intent is unchanged.
* test(fix): align optional-transformers-dependency with onnxruntime ~1.24.3 pin (base #10543)
* docs(fix): sync 150-migration count and document PROXY_LOG_INCLUDE_IPS (base drift #10348/#10507)
* fix(memory): re-check Qdrant health after saving settings
save() optimistically flipped enabled and started the PUT while the mount
effect could immediately GET /api/settings/qdrant/health against the OLD
persisted settings. If that GET won, it returned not_configured/failed and -
because health was non-null - the effect never retried after the PUT
succeeded, leaving a healthy Qdrant red until a manual Test connection.
Invalidate health (generation counter + setHealth(null)) at save start and
after a successful PUT, then explicitly schedule a fresh check: setting
health to null alone is not enough, React bails on the no-op when health is
already null (the exact GET-wins ordering). Stale responses are dropped via
the sequence guard so an in-flight pre-save check can never overwrite the
post-save result. Adds a regression test covering enable ordering.
Addresses PR #10489 review finding (issuecomment-5312271806).
* fix: narrow omniglyph transform result union (merge base aa912c42a typecheck gate)
* test(compression): align contract tests with base aa912c42a merge (providerTransport shape, engine metadata)
* fix(memory): silence set-state-in-effect on Qdrant auto health-check
The health-check re-check fix (3469234) introduced an effect that calls
checkHealth() (an async fetch that eventually calls setState) directly
from a useEffect gated on loading/enabled/health. The
react-hooks/set-state-in-effect rule flags this as a potential cascading
render, matching the same pattern already accepted elsewhere in the
dashboard (FreePoolTab.tsx, ConnectionsTable.tsx) for gated async
data-fetch effects. Suppress with the established inline convention;
no behavior change.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(memory): drop unused set-state-in-effect disable (rule inert on pinned react-hooks 7.0.1)
The eslint-disable-next-line for react-hooks/set-state-in-effect is unused:
eslint-plugin-react-hooks@7.0.1 (lockfile-pinned) does not report this rule,
so the directive itself was flagged as a warning and the 'No new ESLint
warnings' CI gate failed with --max-warnings 0. The effect body only calls
checkHealth() (async fetch) with no raw setState, so no disable is needed.
* ci(quality): sync ratchet configs to release/v3.8.50 (0a74bfbde) merge
- re-freeze open-sse typecheck baseline at merged-tree live counts
(64 stale entries dropped, 11 frozen; base video/usage drift covered)
- register tests/unit/video-bridge-drilldown-route.test.ts in stryker tap.testFiles
- regenerate skills/cli-contexts/SKILL.md (contexts migrate docs from CLI closure)
---------
Co-authored-by: Rouzbeh <rqzbeh@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(usage): wire agentrouter balance quota into dashboard Quota UI (#10078)
* fix(usage): render AgentRouter wallet balance as USD in the Quota UI (#10078)
The prior fix wired AgentRouter's balance into getUsageForProvider() and
USAGE_SUPPORTED_PROVIDERS, but the actual dollar figure never reached the
Dashboard Quota UI: quotas.balance.remaining carried a synthetic two-state
percent (100/0) instead of the real dollarBalance, and the Provider Limits
renderer only formats a row as "$X.XX" when isCredits/currency/creditCount
are set, which the generic quota-parsing path never sets. A configured
balance rendered as a bare "100% left" percentage, not USD.
Shape quotas.balance.remaining as the real USD amount (clamped to 0) and add
an agentrouter branch to quotaParsing.ts that builds a credits-style row
(same buildCreditsQuota() pattern as DeepSeek/Claude extra-usage), so a
configured balance shows a currency-formatted dollar amount and an
exhausted balance always renders as exactly $0.00.
---------
Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
* fix(providers): resolve combo names on /v1/audio/speech and /v1/videos/generations
`GET /v1/models` advertises combos with `owned_by: combo`, and chat, embeddings,
transcriptions (#9134) and images (#8986, #9239) all resolve those names. Speech
and video did not: both rejected a combo name at model validation, before any
resolution could happen.
POST /v1/audio/speech {"model":"my-combo","input":"hi"}
-> 400 Invalid speech model: my-combo. Use format: provider/model
POST /v1/videos/generations {"model":"my-combo","prompt":"a cube"}
-> 400 Invalid video model: my-combo. Use format: provider/model
A client picking a model out of /v1/models therefore could not tell which
entries the catalogue would actually accept, and callers ended up hardcoding
vendor ids for these two routes while using combo names everywhere else.
Both routes now mirror the images route: detect a combo name before the
provider lookup and divert to a strategy executor. The two new executors follow
imageCombo — expand targets with resolveComboTargets(), filter to targets the
route can actually serve, walk them in priority order, and return the first
success or the last failure, with 400/401/403 treated as terminal.
Two details differ from the image strategy:
Speech filters at model level rather than provider level. parseSpeechModel()
resolves a provider prefix without checking that the model behind it can speak,
so `openai/gpt-4o` would otherwise be accepted as a target and fail only once
dispatched. The filter now checks the provider's own model list, and keeps
targets from dynamic provider nodes that do not enumerate models.
Speech also returns the handler's Response untouched instead of building a JSON
body, because that route streams audio; only the ADD-only meta headers are
attached, exactly as the direct path does. The failure branch is the only place
the body is read.
successfulMediaGenerationResponse() gains optional `strategy` and
`fallbackAttempts` so the video strategy can report them the way imageCombo
does, rather than duplicating the cost calculation. Both are omitted on the
direct single-model path, where neither is meaningful.
Tests mirror tests/unit/combo/image-combo.test.ts for both routes: combo not
found, no capable targets, empty combo, and targets present with no provider
connection. 16/16 pass across the three combo test files.
* fix(providers): preserve local overrides, custom models and per-target prompt rules through video combo dispatch
executeVideoCombo() diverged from the direct /v1/videos/generations route in
three ways: it dropped the ComfyUI-style local-override credential lookup for
authType:"none" targets, its capability filter only matched the built-in
video registry (skipping custom OpenAI-compatible provider nodes tagged with
the "videos" endpoint), and the route validated the prompt against the
unresolved combo name before combo targets were expanded — rejecting
prompt-optional I2V targets that never got the chance to opt out.
Extracts the shared resolution rules (resolveVideoModelTarget,
isVideoPromptOptional, resolveLocalOverrideCredentials) into
src/app/api/v1/_shared/videoModelResolution.ts so the direct route and the
combo executor apply identical rules, moves the combo-name diversion ahead of
the prompt-required check so validation runs against the real resolved
target, and adds per-target prompt validation inside the combo loop so a
missing prompt only rules out that target instead of the whole combo.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>