* feat(providers): add OpenAdapter, dit.ai and TokenRouter OpenAI-compatible providers (#4239, #4155, #3841)
Three community-requested OpenAI-compatible aggregators register as standard
named OpenAI-style providers (the zenmux pattern): live /v1/models discovery via
NAMED_OPENAI_STYLE_PROVIDERS, falling back to a seeded catalog on upstream error.
No custom executor/translator — default OpenAI passthrough.
- OpenAdapter https://api.openadapter.in/v1 (free tier) #4239
- dit.ai https://api.dit.ai/v1 (dynamic-pricing) #4155
- TokenRouter https://api.tokenrouter.com/v1 (free MiniMax model) #3841
Base paths confirmed live (each returns a 401 OpenAI-style error body). Seed
catalogs are intentionally minimal (author/doc-cited ids only; TokenRouter
deepseek ids come from production via #3946); full upstream model lists arrive
through live discovery once a key is configured.
* feat(combo): per-step account allowlist for round-robin over a connection subset (#3266)
A combo model step can now carry a first-class `allowedConnectionIds` so a
round-robin / weighted strategy is scoped to a subset of a provider's
connections (e.g. {foo1, foo2}) without hand-pinning one step per account.
- steps.ts: parse `allowedConnectionIds` on the model step (trim + drop empty)
- comboStructure.ts: second writer — propagate the step allowlist onto the
resolved target (tag routing is the first writer)
- autoStrategy.ts: when a step allowlist AND tag routing both apply, intersect
them (most-restrictive wins); empty intersection drops the target
- builderDraft.ts + combos UI: optional 'Restrict to accounts' picker in the
Precision step editor (a pinned single account still takes precedence)
The downstream credential-selection filter (auth.ts) already honours
allowedConnectionIds, so a round-robin scoped to {foo1, foo2} provably never
selects foo3/foo4 (regression test included). Ships the enhancement only; the
#2829 bug-triage half stays open pending the reporter.
* feat(dashboard): category (media serviceKind) filter on the providers page (#4240)
Add a media-category filter row (Image / Video / Music / Text→Speech /
Speech→Text / Embedding) to /dashboard/providers that composes with the existing
search, free-only and 'show configured only' filters.
- serviceKindIndex.ts: client-side resolver unioning a provider's declared
serviceKinds with the registry-derived media kinds (memoised)
- providerPageUtils: filterConfiguredProviderEntries gains a serviceKindFilter
argument; threaded through every provider section on the page
- ProviderSummaryCard: a second chip row drives the serviceKind filter
Membership is derived from the backend media registries, so a provider that
serves a kind is surfaced even when it never declared serviceKinds — keeping the
UI in lockstep with the backend (mirrors the media-providers pages).
* chore(quality): rebaseline file-size for the v3.8.30 harvested features
Four frozen files grew from their own additive feature wiring (#4239/#4155/#3841
providers, #3266 combo allowlist UI, #4240 serviceKind filter):
- src/shared/constants/providers.ts 3169->3213 (3 provider entries)
- src/app/api/providers/[id]/models/route.ts 2554->2560 (3 NAMED set entries)
- src/app/(dashboard)/dashboard/combos/page.tsx 4350->4385 (allowlist picker)
- src/app/(dashboard)/dashboard/providers/page.tsx 1925->1927 (serviceKind state)
All cohesive additive wiring at existing chokepoints; rationale recorded in the
_rebaseline_2026_06_19_v3830_harvest_features key.
* feat(providers): refresh core official model catalogs (sweep lote 1)
Adiciona modelos GA atuais (verificados online) aos provedores oficiais core:
- openai: gpt-5.5-pro, gpt-5.4-pro
- anthropic: claude-opus-4.8 + claude-fable-5 (sampling fixo 4.7+, espelha 4.7), claude-opus-4.5
- groq: qwen/qwen3.6-27b, openai/gpt-oss-safeguard-20b
- xai: grok-build-0.1
Fase 4 do provider-model-sweep. provider-consistency/file-size/typecheck:core verdes.
* feat(providers): wire live /models discovery for 7 openai-style providers (sweep lote 2)
venice, deepinfra, wandb, pollinations, nscale, inference-net and moonshot each
expose a real live `<baseUrl>/models` catalog (the sweep probed each upstream),
but were classified fixed-official, so import served their small hardcoded seed
and re-staled the catalog. Add them to NAMED_OPENAI_STYLE_PROVIDERS so import does
a live `<baseUrl>/models` fetch, keeping the registry seed only as the offline
fallback — same fix shape as #4249 (vercel-ai-gateway) / #4202 (zenmux) / #3976
(llm7/byteplus). siliconflow was already classified.
TDD regression in tests/unit/provider-sweep-live-discovery.test.ts pins each
derived /models URL + the local-seed fallback path. file-size baseline bumped
2538->2548 (+10 = 7 Set entries + 3-line comment; not extractable).
* feat(providers): wire live /models discovery for 12 aggregator marketplaces (sweep lote 3)
crof, featherless-ai, ovhcloud, sambanova, orcarouter, uncloseai, opencode-go,
baseten, hyperbolic, nebius, scaleway and together are GPU-cloud / aggregator
marketplaces hosting large, volatile OSS catalogs. The sweep probed each and
confirmed a live `<baseUrl>/v1/models` endpoint (200 public or 401/403 = exists +
keyed), yet they were classified fixed-official and served a small hardcoded seed.
Add them to NAMED_OPENAI_STYLE_PROVIDERS so import does a live `<baseUrl>/models`
fetch (graceful fallback to the registry seed on any upstream error), keeping the
catalog fresh instead of re-staling a hardcoded list.
Extends tests/unit/provider-sweep-live-discovery.test.ts to 20 cases pinning each
derived /models URL. file-size baseline bumped 2548->2564 (+16; not extractable).
* feat(providers): add verified new models to nvidia, meta-llama, morph (sweep lote 4)
Curated first-party / specialist menus (kept hardcoded — their per-model flags
like toolCalling/supportsReasoning can't be inferred from a live catalog):
- nvidia: + stepfun-ai/step-3.7-flash, deepseek-ai/deepseek-v4-flash
(supportsReasoning), moonshotai/kimi-k2.6 — all confirmed present in the live
NIM /v1/models catalog. minimaxai/minimax-m3 deliberately left out per #3329
(now listed, but its inference still needs confirmation before re-adding).
- meta-llama: + Llama-3.3-8B-Instruct.
- morph: + morph-qwen35-397b, morph-minimax27-230b, morph-qwen36-27b,
morph-dsv4flash (Morph-hosted fast models, with context lengths).
Skipped this batch after review: upstage solar-pro2 (older than the solar-pro3
already in the registry); longcat LongCat-2.0-Preview (deliberately commented out).
* feat(providers): refresh Chinese first-party model catalogs, online-verified (sweep lote 5)
Each registry held a single stale id; refreshed against official docs after
per-id online verification (subagent research, cross-checked against first-party
sources). Rejected/omitted entries are documented inline.
- baidu: + 15 ERNIE ids (5.0/5.1 are the current flagships, confirmed live on Qianfan).
- doubao: + 8 Seed-2.0/1.x dated Ark ids (Seed 2.0 GA 2026-02-14, confirmed real).
- sensenova: + 8 SenseChat/SenseNova ids (V6.5-Pro flagship; 6.7-flash-lite lowercase).
- tencent: + hunyuan-turbos-latest/t1-latest/vision/functioncall/lite. Dropped legacy
standard/-256K/code/role + pinned turbos-20250226. NOTE: legacy Hunyuan platform
EOLs turbos/t1 on 2026-06-22 (migrating to TokenHub/hy3-preview) — revisit.
- baichuan: + Baichuan4-Turbo/Air, Baichuan3-Turbo/-128k (official pricing page).
- stepfun: + step-3.7-flash (flagship), step-3.5-flash(-2603), step-1o-turbo-vision.
- iflytek: + 4.0Ultra, max-32k, generalv3, pro-128k, lite (exact HTTP domains).
- sparkdesk: + 4.0Ultra, generalv3, pro-128k. Rejected spark-x (separate /v2|/x2 endpoint).
- volcengine: + doubao-seed-2-0-pro-260215, kimi-k2-5-260127 (Ark-hosted).
* feat(providers): add verified models to kie, nlpcloud, publicai (sweep lote 6)
- kie: + claude-opus-4-8, gemini-3-5-flash (current flagships the proxy surfaces;
gemini-3-pro skipped — registry already carries the newer gemini-3-1-pro).
- nlpcloud: + chatdolphin, dolphin (branded models), finetuned-llama-3-70b,
llama-3-1-405b. Host confirmed reachable.
- publicai: + Apertus-8B, Gemma-SEA-LION-v4-27B, Olmo-3-7B, EuroLLM-22B (open models).
Skipped after review: minimax M2/M2.1 (older than the M2.5 floor the registry
curates); yi (api.lingyiwanwu.com degraded + 01.AI exited foundation models);
llamagate (host llamagate.ai unreachable, code 000) — both flagged for Track C.
* feat(providers): finish Track B tail — cloudflare-ai, bailian, suno, +5 (sweep lote 7)
- cloudflare-ai: + 7 Workers AI catalog ids (llama-3.3-70b-fp8-fast, qwen2.5-coder-32b,
qwq-32b, llama-3.2-3b, glm-4.7-flash, kimi-k2.6, gemma-4-26b).
- bailian-coding-plan: + qwen3.7-plus, qwen3-coder-plus, qwen3-coder-next, glm-4.7.
- suno: + chirp-fenix (V5.5), chirp-crow (V5).
- monsterapi: + Meta-Llama-3.1-8B, Llama-3.3-70B.
- huggingchat: + Qwen3-235B-A22B, Mistral-Small-3.1-24B.
- vertex-partner: + claude-opus-4-8, claude-opus-4-6.
- puter: + google/gemini-3.5-flash.
- codestral: + codestral-2508.
Skipped after verification: windsurf + devin-cli — docs.devin.ai exposes DASHED ids
(claude-opus-4-8-low, MODEL_PRIVATE_4 for "Grok Code Fast 1", minimax-m2-5) while the
registry uses DOTTED (claude-opus-4.7-max); id-form ambiguity needs owner confirmation
before adding 13+ entries. leonardo/ideogram (image UUID-vs-friendly convention),
glmt (shared GLM_SHARED_MODELS, redundant with the live `glm` provider).
* fix(providers): drop retired models, add codestral-2405 forward (sweep lote 8, Track C C1)
Confirmed removals that interacted with the sweep's adds:
- codestral: drop codestral-2405 (retired 2025-06-16, Mistral official docs) from the
menu + add a codestral-2405 -> codestral-2508 deprecation alias so old configs forward.
- monsterapi: drop llama-3-8b-fuse (no longer evidenced in the catalog).
- volcengine: drop kimi-k2-thinking-251104 (retired on Ark; superseded by kimi-k2-5-260127).
* chore(providers): mark 6 dead providers deprecated (sweep lote 9, Track C C2)
The sweep verified these providers are no longer reachable/operational, so flag
them with the existing deprecation mechanism (deprecated:true + a deprecation risk
notice) instead of silently offering non-working options. Conservative — plumbing
(executors/icons/free-catalogs) is left intact; only the UI-facing metadata changes.
- kluster, glhf, predibase, inclusionai, galadriel: api host DNS no longer resolves.
- phind: API shut down 2026-01 (www.phind.com/api/chat no longer serves).
Not touched: gemini-cli (Google OAuth infra still live), qwen (already deprecated),
chipotle (easter-egg, out of scope). file-size baseline bumped 3169->3198.
* fix(providers): replace retired LongCat-Flash line with LongCat-2.0-Preview (sweep lote 10)
The LongCat-Flash-* models (Lite/Chat/Thinking/Omni-2603) were officially retired
2026-05-29; the current longcat.chat/platform docs expose only LongCat-2.0-Preview
(confirmed via WebFetch of the live API docs). Swap the stale 4-model seed for the
single current model so the provider stops offering dead ids.
* chore(quality): reconcile antigravity.ts file-size baseline 1664->1680
#4309 (Undici socket-leak fix) grew antigravity.ts by +26 lines but its file-size
baseline was not bumped at merge time; reconcile it here on the combined tree so the
release file-size gate stays green (Rule #9, release-volatile reconciliation).
* feat(quality): instala @axe-core/playwright + allowlist (T13)
Pré-requisito do gate de a11y real. O spec tests/e2e/a11y.spec.ts já tem o
mecanismo REQUIRE_AXE=1 que falha se o pacote estiver ausente quando exigido —
faltava só o pacote + o job nightly + o baseline real.
- @axe-core/playwright@^4.11.3 em devDependencies (via --package-lock-only,
não toca o node_modules compartilhado das worktrees)
- adicionado à dependency-allowlist (check:deps OK, 127 deps)
- vuln-ratchet OK (1 moderate pré-existente, baseline 10 — axe não regride)
* feat(quality): job a11y nightly (REQUIRE_AXE=1) + gate per-PR (T13)
- novo job 'a11y' em nightly-resilience.yml: o webServer do Playwright builda o
Next (build-next-isolated.mjs) e sobe o standalone sozinho (sem artefato pré-
buildado como o test-e2e do ci.yml), REQUIRE_AXE=1 roda a análise axe real.
- gate de nightly no spec: a11y.spec.ts é casado pelo glob 'tests/e2e/*.spec.ts'
do job per-PR (9 shards); sem gate, instalar @axe-core/playwright ligaria axe
em TODO PR (e falharia em baseline 0). Os 4 testes de página agora skipam a
menos que REQUIRE_AXE=1 — per-PR fica rápido/verde, nightly roda real.
- zizmor 139->145: +3 drift pré-existente da base a23d0d678 (release fast-path
não ratcheta workflows) + 3 do job novo (checkout/setup-node/cache @vN, mesma
convenção deliberada e INTOCADA de todos os workflows). Ver nota no baseline.
* feat(quality): congela baseline real de violações a11y (T13)
Medido no primeiro run nightly (27852779527, REQUIRE_AXE=1): /login=1,
/dashboard=4, /dashboard/providers=3, /dashboard/settings=5. O webServer
buildou+subiu o app e o axe rodou nas 4 páginas; a falha do run foi só o
assert vs baseline 0 (esperado), não infra.
* feat(quality): config stryker disableBail para medir redundância (R1)
Estende stryker.conf.json com disableBail:true (killedBy lista TODOS os killers,
não só o primeiro) + incremental:false (medição limpa). One-off, não toca o
nightly — só roda via mutation-redundancy.yml.
* feat(quality): workflow on-demand de mutação disableBail (R1)
mutation-redundancy.yml (workflow_dispatch): roda os 6 leaf-batches combo+chatCore
do nightly com o override disableBail e sobe os reports. Batches granulares (d/e/f/
g/h/i) em vez de 2 mega-batches: disableBail é mais caro e Stryker só grava o report
se COMPLETAR, então cada batch cabe nos 300min.
zizmor 139->145: +3 drift de base + 3 do workflow (@vN, convenção do repo). Ver nota.
* feat(quality): mutation-radiography --candidates (lista de prune R1)
Reusa aggregateRadiography (DRY) em vez de um script novo: redundancyCandidates()
retorna 🔴 empty ∪ 🟠 redundant (zero kills únicos) = candidatos a prune. Sob
disableBail o killedBy é completo, então 🟠 redundant fica ACURADO. CLI --candidates
emite a lista com o aviso do gate humano (excluir segurança/contrato/repro).
The CLI-tools MITM card's "How it works" section showed a single hardcoded example
domain (only antigravity/kiro). It now lists every 127.0.0.1 <host> entry for the
selected tool, so users on locked-down machines — where the automatic, sudo-gated
hosts-file edit isn't available — can add them manually. Hosts come from a client-safe
projection of the canonical src/mitm/targets registry (MITM_TOOL_HOSTS), kept in
lock-step with the registry by a sync test (no duplicated source of truth).
Co-authored-by: mrcyclo <13806369+mrcyclo@users.noreply.github.com>
* fix(sse): release reader and cancel stream on abort/error to prevent Undici pool socket leak
* test(sse): regression guard for Undici socket release on abort (#4309)
Proves collectStreamToResponse releases the reader and cancels the response
body on the abort/error path (fails without the fix, passes with it) — Rule #18.
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
---------
Co-authored-by: Dmitry Kuznetsov <dmitry@kuznetsov.me>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
* fix(deps): bump undici to 7.28.0 and dompurify to 3.4.11 (security) (#4304)
* fix(deps): bump undici to 7.28.0 and dompurify to 3.4.11 (security)
Resolves Dependabot alerts on package-lock.json and electron/package-lock.json:
- undici 7.x -> 7.28.0: TLS certificate validation bypass via dropped requestTls in SOCKS5 ProxyAgent (GHSA-vmh5-mc38-953g, HIGH) + cross-user information disclosure via shared-cache whitespace bypass (GHSA-pr7r-676h-xcf6, MEDIUM). Fixed in the root (jsdom transitive) and electron lockfiles.
- dompurify -> 3.4.11: permanent ALLOWED_ATTR pollution via setConfig() bypassing the hook clone-guard (GHSA-cmwh-pvxp-8882, MEDIUM). Bumped the overrides floor from ^3.4.9 to ^3.4.11.
Also bumps node-gyp's transitive undici 6.26.0 -> 6.27.0, clearing the <6.27.0 advisories (WebSocket DoS, Set-Cookie handling) surfaced by npm audit. Lockfile/override-only change; no production source touched.
* ci(quality): exclude dependency manifests/lockfiles from PR test-policy
The PR test-policy gate classifies any changed file under src/, open-sse/, electron/, or bin/ as production code requiring tests. This false-flags lockfile/manifest-only changes (e.g. this Dependabot security bump touching electron/package-lock.json), since a lockfile cannot have a meaningful unit test.
Adds package.json / package-lock.json to EXCLUDED_PATTERNS, consistent with the existing .md/.yaml/.yml exclusions. Real production-code changes remain flagged.
* fix(kiro): emit early role-only start chunk to release stream-readiness gate
CodeWhisperer sends framing/metadata frames before the first content token; on large/agentic contexts that gap can be many seconds. ensureStreamReadiness holds the whole response from the client until it sees a useful SSE frame, so without an early frame the client sees a frozen connection up to STREAM_READINESS_TIMEOUT_MS (180s in VibeProxy) then a burst. Emit a role-only chat.completion.chunk on the first parsed AWS EventStream frame (a non-ping structured payload that satisfies hasStreamReadinessSignal) to hand off immediately, mirroring Claude message_start / OpenAI response.created.
---------
Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
* fix(quality): complexity gate varre bin/ + electron (6A.11, fake-green fix)
ESLINT_ARGS passava só 'src open-sse', mas o config eslint.complexity.config.mjs
e o complexity-baseline.json já documentavam o escopo src+open-sse+electron+bin.
A edição do scan nunca tinha sido aplicada: o gate alegava cobrir bin/electron e
nunca os varria (fake-green — uma god-function nova em bin/ passava verde).
- exporta ESLINT_ARGS + adiciona 'electron' e 'bin' (casa o config)
- teste de build trava o escopo do scan
- baseline 1887->1888: electron+bin medem 0 (widening 0-custo); o +1 é drift
pré-existente de src/open-sse da base a23d0d678 (#4308 et al.), não do widening
* fix(quality): check:tracked-artifacts no pre-commit (6A.12)
O gate estava no CI + pre-push, mas faltava no pre-commit — e o incidente do
symlink (artefato de build trackeado) acontece no 'git add'. Fecha o buraco no
ponto mais cedo.
* fix(translator): clamp Responses API call_id to 64 chars (port from 9router#396)
The OpenAI Responses API rejects call_id values longer than 64 characters with
a 400. Long upstream tool-call ids (some clients emit ids well over the limit)
were forwarded verbatim. Clamp the id deterministically on both the
function_call item and its matching function_call_output, so the pair stays
matched through the orphaned-output filter and the request is accepted.
Reported-by: ngapngap (https://github.com/decolua/9router/issues/393)
Co-authored-by: Anurag Saxena <17893081+anuragg-saxenaa@users.noreply.github.com>
Co-authored-by: ngapngap <27039619+ngapngap@users.noreply.github.com>
* chore(quality): bump translator-openai-responses-req file-size baseline 1011->1047
The clamp-call_id regression test (+36 lines) grew the test file past its frozen
baseline; bump it in the same change (Rule #9).
---------
Co-authored-by: Anurag Saxena <17893081+anuragg-saxenaa@users.noreply.github.com>
Co-authored-by: ngapngap <27039619+ngapngap@users.noreply.github.com>
GitHub Copilot is a public device-flow OAuth client (client_id, no client_secret),
but the github provider config never populated clientId. The standalone refresh path
omitted client_id (buildFormParams drops undefined) and the executor path sent the
literal "client_id=undefined&client_secret=undefined" — both rejected by GitHub, so a
Copilot connection got stuck once its short-lived token expired and the long-lived
refresh path was needed. Populate the provider clientId from the embedded public cred
(resolvePublicCred, never a literal) and only send client_secret when one exists. The
prior github refresh test patched a fake clientId/clientSecret onto PROVIDERS.github,
masking the broken real config — it now exercises the real config.
Co-authored-by: Manuel B. <1494154+baslr@users.noreply.github.com>
The proxy assignments list returned by /api/settings/proxies/assignments is
global, so its first entry belongs to some other scope. ProxyConfigModal picked
`items.find(matchingScope) || items[0]`, so opening the proxy config for a freshly
created provider/key (which has no assignment of its own) fell back to items[0]
and pre-filled host/port/user/password from an unrelated proxy plus set
hasOwnProxy=true — users reported a new provider already carried a proxy they
never configured.
Extracted the scope helpers into proxyAssignment.ts and added selectScopeAssignment
which returns null (never items[0]) when the current scope has no assignment. The
modal then shows the empty/custom state for new scopes. Both call sites now use it.
TDD: src/shared/components/proxyAssignment.test.tsx (no-match -> null red->green for
provider/key/global scopes; matching-scope + empty-list regression guards). Existing
ProxyConfigModal component test stays green.
Inner.ai's live model list is plan-gated, and findModel() fell back to
models[0] (the first live model, typically gpt-4o) whenever the requested
model did not match by exact/case-insensitive/substring. That silently
rerouted every model not exposed by the plan to gpt-4o, so users reported
that only gpt-4o ever responded.
findModel() now returns null on no match; the single caller already builds
a synthetic entry carrying the actually-requested model name, so the request
is sent for the model the user asked for and Inner.ai can surface a clear
error if the plan does not expose it.
TDD: tests/unit/inner-ai-find-model.test.ts (no-match -> null red->green;
exact / case-insensitive / substring / empty-list regression guards).
The default direct dispatcher pools keep-alive sockets for up to
fetchKeepAliveTimeoutMs (4s). Edges like nvidia / opencode-zen silently
close idle keep-alive sockets within that window, so the next request
reusing a pooled socket fails with UND_ERR_SOCKET ("other side closed") —
in bursts. proxyFetch retried once, but the retry reused the SAME pooled
dispatcher and could grab another stale socket, then fell through to native
fetch (which also pools) → the job sat in the rate-limit queue until the
30s timeout → 502 + circuit breaker open.
Add getRetryDispatcher() (no keep-alive, no pipelining, mirrors the proxy
dispatcher mitigation) and use it for the retry attempt so it opens a fresh
socket that can't be a dead pooled one. The first attempt still uses the
pooled dispatcher, preserving healthy keep-alive reuse.
Regression test (DI mock) asserts the retry uses getRetryDispatcher(), not
getDefaultDispatcher() (RED before, GREEN after).
Refs #4281 (describeFetchCause diagnostics). Closes#4252
The #2101 guard that detects a body-specific 400 (context overflow /
malformed / model-access-denied) logged "stopping combo" but executed a
bare `break`, which only exits the inner retry loop. executeTarget then
returns null, and the outer target loop treats null as "this target
produced nothing" and advances to the next model — so the guard never
actually stopped fallback, and a combo of N targets that all reject the
same request body tried all N (the report shows a 143-model Codex combo
marching through every target).
Surface the 400 via the {ok,response} contract (mirrors the 499
client-disconnect path) so the outer loop resolves the combo and stops.
Regression test: a 3-target priority combo whose targets all return a
body-specific 400 must stop after target 1 (RED before, GREEN after).
Closes#4279
Pollinations now requires API keys for premium models (claude, gemini, midijourney). The executor surfaces an actionable 401 with the keyless-model list, chatCore preserves the upstream HTTP status (401 -> authentication_error instead of 502), and the free catalog marks the premium models as key-required. Rebased onto the release tip and reconciled the file-size baseline (chatCore 5128). Thanks @oyi77.
A Responses-API target (codex/cx) streams from upstream even on stream:false.
Its terminal `response.completed` snapshot can carry a non-empty `output` that
lacks the assistant message item (e.g. only a reasoning item) even though the
streamed output_text deltas reconstructed a full message. parseSSEToResponsesOutput
preferred the terminal output wholesale, dropping the reconstructed text → empty
content on stream:false (hit via n8n, which defaults to stream:false).
Fall back to the reconstructed delta output when the terminal output has no
message item but the reconstruction does; the terminal snapshot still wins when
it already carries the message.
Regression test feeds a synthetic codex SSE (reasoning-only terminal + message
deltas) and asserts the assistant text survives, plus a control case where the
terminal carries the message.
Closes#3948
The native-Claude OAuth anti-fingerprint cloak renames a tool named `read`
to `Read` on the wire and records the reverse alias on a non-enumerable
`_toolNameMap`, which the response side un-cloaks to restore the client's
original casing. Since v3.8.27 (#3941/#3968) `execute()` returned a
JSON-round-tripped `serializedBody` as `transformedBody`; the round-trip
drops the non-enumerable map, so the restore saw an empty map and the
cloaked `Read` streamed verbatim to the client.
Re-attach the live `_toolNameMap` onto the serialized body before returning
(non-enumerable, mirrors antigravity.ts::attachToolNameMap) so tool-name
casing round-trips correctly.
Regression test exercises base.ts execute() through the claude-OAuth cloak
path and asserts the returned transformedBody carries the reverse map.
Closes#4307
Cache HITs now report Response-Cost 0 (incremental) and surface the avoided cost in X-OmniRoute-Cost-Saved. MISS path unchanged. TDD + per-key isolation guard.
Resolves Dependabot alerts on package-lock.json and electron/package-lock.json:
- undici 7.x -> 7.28.0: TLS certificate validation bypass via dropped requestTls in SOCKS5 ProxyAgent (GHSA-vmh5-mc38-953g, HIGH) + cross-user information disclosure via shared-cache whitespace bypass (GHSA-pr7r-676h-xcf6, MEDIUM). Fixed in the root (jsdom transitive) and electron lockfiles.
- dompurify -> 3.4.11: permanent ALLOWED_ATTR pollution via setConfig() bypassing the hook clone-guard (GHSA-cmwh-pvxp-8882, MEDIUM). Bumped the overrides floor from ^3.4.9 to ^3.4.11.
Also bumps node-gyp's transitive undici 6.26.0 -> 6.27.0, clearing the <6.27.0 advisories (WebSocket DoS, Set-Cookie handling) surfaced by npm audit. Lockfile/override-only change; no production source touched.
First full mutation measurement landed (run 27823984918, the split nightly from #4272):
31 modules now have a COVERED mutation score. T3 turns that into an enforced gate.
Seed: 31 `mutationScore.<path>` floors in quality-baseline.json at ~2pt below the measured
score (absorbs run-to-run variance), direction:up, dedicatedGate:true. dedicatedGate means
the generic check-quality-ratchet SKIPS them (check-quality-ratchet.mjs:62) — they are
enforced only by check-mutation-ratchet.mjs. Range: memorySkillsInjection 13.49 (weakest)
to headers 94.29 (strongest); the security/critical floors: auth 52.57, accountFallback
68.38, routeGuard 76.08, circuitBreaker 56.94, error 43.83, publicCreds 59.76.
Gate: a new `mutation-ratchet` job in nightly-mutation.yml runs AFTER all batches
(needs: stryker, if: always()), downloads every mutation report, and ratchets the MERGED
per-module scores with `check-mutation-ratchet --ratchet` (blocking). It must aggregate
because the split batches each emit a PARTIAL view of a file (auth.ts in a1+a2,
accountFallback in b1+b2) — a per-batch ratchet would compare half a file against the
whole-file floor. check-mutation-ratchet unions same-file mutants across reports (#4272).
A module dropping below its floor fails the run; missing reports (upload flake) are skipped.
Verified: ratchet exits 0 on the seeded measurements, exits 1 on a synthetic regression
(auth 33.33 < 52.57), exits 0 advisory without --ratchet. Baseline change is additive
(31 floors + one comment; existing keys untouched). check-mutation-ratchet tests 8/8.
The Gemini CLI is not OpenAI-compatible — it speaks the native Gemini API.
OmniRoute exposes a Gemini-native surface at /v1beta, so setup-gemini emits the
@google/genai env recipe (GOOGLE_GEMINI_BASE_URL root + GEMINI_API_KEY) and
optionally writes ~/.gemini/settings.json (model). Remote-aware (resolves
baseUrl + key from the active context, --remote or --api-key). Documents the
cached-Google-login caveat that can override the base URL.
Completes the per-CLI setup series (Codex, Claude, OpenCode, Cline, Kilo,
Continue, Cursor, Roo, Crush, Goose, Qwen, Aider, Gemini).
The Antigravity IDE speaks cloudcode (the Gemini payload wrapped under
`request`) and expects a cloudcode reply ({response:{candidates}}). The
AgentBridge proxy forwarded that envelope verbatim to /v1/chat/completions
(OpenAI), which 400s on the missing `messages` field — so the IDE could be
decrypted/intercepted but never actually routed to a provider.
Wire the inbound cloudcode path, reusing the already-registered bidirectional
translators (no new translators needed):
- provider.ts: detectFormatFromEndpoint classifies the /antigravity path as
sourceFormat "antigravity" (mirrors /messages -> claude), so the pipeline
translates request antigravity->openai and response openai->antigravity.
- /v1/antigravity route (new): cloudcode-compatible endpoint — just calls
handleChat (mirrors /v1/messages).
- server.cjs: routes cloudcode envelopes to /v1/antigravity (translates both
ways) and plain OpenAI bodies to /v1/chat/completions, via a testable shim.
Tests: forward-target shim (cloudcode vs openai routing) + endpoint format
detection. The antigravity<->openai translators are already covered by
translator-antigravity-to-openai / translator-resp-openai-to-antigravity.
Stacked on #4285 (Gap A). Full Antigravity-IDE e2e validates on the next
standalone deploy (provider.ts + the route compile into .next).
CLI #12 of the series. `omniroute setup-aider` writes Aider's ~/.aider.conf.yml
(openai-api-base = ROOT url, NO /v1 — LiteLLM appends /v1/chat/completions —
model: openai/<id>), merges to preserve existing config, and prints the env
recipe (OPENAI_API_BASE + OPENAI_API_KEY in the env, never the file) plus the
headless command (aider --message ... --yes). Remote-aware; model via --model or
interactive pick.
Researched against aider.chat: OpenAI-compatible via OPENAI_API_BASE (base, no
/v1) + --model openai/<id>. Aider's wire (/v1/chat/completions) already validated → "OK".
Tests: resolveAiderTarget (/v1 strip, key), buildAiderConfig (openai-api-base +
openai/<model> + preserve), buildAiderRecipe (env-ref key + headless). 4 unit tests; cli-i18n green.
* fix(dashboard): improve api try it functionality and allow manual key entry
* test(api): cover generateExampleFromSchema for the Try It panel (#4296)
Export generateExampleFromSchema from the /api/openapi/spec route and add a
unit test covering type handling, property-name heuristics, $ref/oneOf/anyOf/
allOf resolution, the 'required + first 3 optional' object policy, and the
depth-3 recursion guard — the example bodies the dashboard Try It panel
pre-fills. Rule #18 regression guard for the new helper.
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
---------
Co-authored-by: ci <ci@local>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
* fix: polyfill crypto.randomUUID for non-secure contexts
crypto.randomUUID() requires a secure context (HTTPS or localhost).
When accessing the dashboard over HTTP on a LAN IP, the function is
undefined, causing 'Failed to add account' errors on providers that
generate account IDs client-side (e.g. mimocode).
Adds a lightweight polyfill that falls back to a Math.random()-based
UUID v4 generator when the native API is unavailable.
* fix: address review comments on crypto.randomUUID polyfill
- Use crypto.getRandomValues() for cryptographic security instead of Math.random()
- Add typeof window !== 'undefined' guard to avoid ReferenceError in non-browser envs
- Use window.crypto for safe access instead of bare crypto reference
- Replace var with const and == with === for modern JS syntax
- Add fallback to Math.random() when getRandomValues is unavailable
- Add unit tests verifying valid UUID v4 format, version/variant nibbles,
uniqueness, and preference for getRandomValues over Math.random
* test(dashboard): regression guard for crypto.randomUUID polyfill (#4287)
Reads src/app/layout.tsx and asserts the blocking inline script installs a
guarded window.crypto.randomUUID polyfill (RFC4122 v4 shape, getRandomValues
preferred with a Math.random fallback). Fails on the pre-fix tree (no polyfill),
passes with the fix — Rule #18 regression guard for the non-secure-context
(HTTP/LAN-IP) dashboard breakage.
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
---------
Co-authored-by: pizzav-xyz <pizzav-xyz@users.noreply.github.com>
Co-authored-by: ci <ci@local>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
The compression "ultra" SLM tier (#4257) runs @atjsh/llmlingua-2 + transformers + tfjs
+ js-tiktoken in a worker thread shipped under dist/. These are optionalDependencies
installed into the ROOT node_modules on --include=optional, but the Next.js standalone
trace bundles ONLY @huggingface/transformers (3.5.2, pinned) into dist/node_modules —
not the dynamically-imported optionals.
Result: the worker resolves transformers from dist/node_modules (3.5.2) for its env
config but resolves @atjsh/llmlingua-2 from the ROOT, whose own transformers import
hits a DIFFERENT instance. The cacheDir config never reaches the instance llmlingua-2
uses, so the local model never loads and the SLM tier silently fails-open (and on a
root transformers 4.x, llmlingua-2 throws on the tokenizer API change).
Fix: postinstall co-locates the SLM optional closure from the root node_modules into
dist/node_modules (no-clobber, so the pinned dist transformers/onnxruntime stay), so
the worker resolves a single 3.5.2 instance and the local model loads.
VPS-validated (Rule #18): the co-located layout produced real 54.8% compression
(11520->5203 chars) via real ONNX inference on the production host, both the default
and the #4257 modelPath code paths.
- scripts/build/colocateOptionals.mjs: closure walk (deps+optionalDeps, skips the
transformers peer) + no-clobber co-location; idempotent + fail-soft
- wired into scripts/build/postinstall.mjs next to ensureSwcHelpers
- registered in package.json files + pack-artifact allow/required lists
- tests/unit/colocate-optionals.test.ts: closure, no-clobber, idempotence, gates
- docs/ops/RELEASE_CHECKLIST.md: note the auto co-location
The standalone server.cjs proxy intercepts AgentBridge requests inline (no
MitmHandlerBase / agentBridgeHook), so intercepted traffic never reached the
TS hook that pushes into globalTrafficBuffer — the Traffic Inspector stayed
empty for AgentBridge even on successful intercepts. Three gaps closed:
- _internal/ingest.cjs (new): pure payload builder + fire-and-forget poster
(never throws — capture must not break proxy traffic).
- server.cjs: intercept() accumulates response (bounded) + status/headers and
posts the captured entry to the local-only /internal/ingest endpoint in a
finally block; also captures error/4xx intercepts.
- manager.ts: resolves the ingest token via getIngestTokenForBootstrap() and
passes it to the spawned proxy so the endpoint accepts the post.
- authz management policy: exempt the loopback /internal/ingest endpoint from
management auth — it has its own shared-secret token gate, and server.cjs
has no dashboard cookie. Stays strictly loopback (LOCAL_ONLY gate unchanged).
- ingest route: masks secrets / strips hop-by-hop headers before buffering
(server.cjs sends raw over the token-gated loopback) — Hard Rule #12.
Tests: ingest shim (build/post/no-token/error) + route sanitization + 403 +
management-policy carve-out (loopback allow / remote LOCAL_ONLY).
CLI #10 of the series. `omniroute setup-goose` writes Goose's file-based
~/.config/goose/config.yaml (GOOSE_PROVIDER=openai, GOOSE_MODEL=<model>,
OPENAI_HOST=<root, NO /v1 — Goose appends the path itself>), merges to preserve
existing keys, and prints the guaranteed env-var recipe (the key lives in the env
/ OS keyring, never the config). Remote-aware (--remote/--api-key → context →
localhost); model via --model or interactive pick.
Researched against block/goose: provider openai + OPENAI_HOST base (no /v1).
Goose's wire (/v1/chat/completions) already validated → "OK".
Tests: resolveGooseTarget (/v1 strip, key), buildGooseConfig (provider/model/host
+ preserve), buildGooseEnvRecipe (env-ref key). 4 unit tests; cli-i18n green.
* feat(memory): x-omniroute-no-memory opt-out + memory off-by-default + token-cost UI alert
PRD-2026-06-19-no-memory-header. The gateway injects up to memorySettings.maxTokens
(~2k) of memory (and skills) context into every chat call for memory-enabled keys,
inflating tokens+cost ~137x for clients that manage their own context (e.g. Omniflow).
Three changes:
- A) x-omniroute-no-memory request header (mirrors x-omniroute-no-cache): when truthy
(true/1/yes), skip memory+skills injection for that request. New pure helper
isNoMemoryRequested() in chatCore/headers.ts; chatCore passes memoryOwnerId=null on
opt-out (a null owner disables both injection branches).
- B) Memory OFF by default: DEFAULT_MEMORY_SETTINGS.enabled true->false. Enabling injects
billed context per request, so it's now an explicit opt-in. Installs that already
enabled it keep it; unset installs default off (no migration seeds memoryEnabled).
- C) Settings -> Memory shows a token-cost warning callout when memory is enabled
(new settings.memoryTokenCostWarning i18n key, interpolating the configured maxTokens).
Tests: no-memory-header.test.ts (5, helper truthiness/case/Headers); memory-settings-default
and chatcore-memory-skills-injection aligned to the new off-by-default. 65/65 memory+chatcore
tests green; typecheck/lint/file-size/i18n(@65) clean.
* test(memory): enable memory in memory-tools test (memory now off by default)
The full CI unit suite flagged memory-tools.test.ts 'memory search ...' failing
after DEFAULT_MEMORY_SETTINGS.enabled flipped to false: omniroute_memory_search
routes through retrieveMemories, which returns [] while memory is disabled
(enabled:false → maxTokens 0). The memory MCP tools operate within the memory
subsystem, so the test now enables memory explicitly (updateSettings + cache
invalidation) — the realistic precondition for a client using the tools.
Aligns the test to the intentional off-by-default change; assertions unchanged.
CLI #8 of the series. Roo Code (RooVeterinaryInc.roo-cline, a Cline fork) keeps
live settings in opaque VS Code globalStorage, but supports Settings Import and
an `roo-cline.autoImportSettingsPath` (VS Code settings.json) that loads a JSON
at startup.
`omniroute setup-roo`:
- writes ~/.omniroute/roo-settings.json — a Roo provider profile
(providerProfiles.apiConfigs.OmniRoute: apiProvider=openai, openAiBaseUrl WITH
/v1 — Roo appends /chat/completions — openAiApiKey, openAiModelId).
- sets roo-cline.autoImportSettingsPath in VS Code settings.json when present
(preserves other settings).
- prints the guaranteed UI path (Settings → Providers → OpenAI Compatible) +
the "Import Settings" fallback.
- remote-aware; model via --model or interactive pick.
Researched against current Roo docs: OpenAI-compatible needs baseUrl WITH /v1 and
native tool-calling (OmniRoute supports it). Roo's wire (/v1/chat/completions)
already validated → "OK".
Tests: resolveRooTarget (/v1, key), buildRooImport (provider profile + /v1 + key
fallback), buildRooVscodeAutoImport (pointer + preserve). 5 unit tests; cli-i18n green.
CLI #7 of the series. Cursor stores its OpenAI key + "Override OpenAI Base URL"
in an opaque SQLite DB (state.vscdb) with no stable schema — not safe to
file-write. So `omniroute setup-cursor` prints the exact in-app steps and lists
real model names from /v1/models.
- Resolves apiBase WITH /v1 (Cursor appends /chat/completions) + key from
--remote/--api-key → active context → localhost.
- Prints Settings → Models → Override OpenAI Base URL + key + model-name steps,
with a clear caveat that the custom base URL powers Cursor's CHAT panel only
(Composer / inline-edit / autocomplete stay on Cursor's backend).
Researched against current Cursor behavior. Tests: resolveCursorTarget (/v1, key),
buildCursorInstructions (base URL + /v1 note + model samples + caveat). 4 unit
tests; check:cli-i18n green.
CLI #6 of the series. `omniroute setup-continue` writes Continue's file-based,
mergeable ~/.continue/config.yaml (shared by the VS Code/JetBrains extensions AND
the `cn` CLI) from the live model catalog.
- Each curated model → a Continue model entry: provider: openai, model: <id>,
apiBase WITH /v1 (Continue appends /chat/completions), apiKey:
${{ secrets.OMNIROUTE_API_KEY }} (secret referenced, never written), roles
[chat, edit, apply] (+ autocomplete for the fast tier).
- Merges into existing config.yaml (js-yaml load/dump): drops prior models on the
same apiBase, preserves the user's other models + top-level keys.
- Remote-aware (--remote/--api-key → active context → localhost); --only filter.
- Prints how to provide the key (shell env for cn; ~/.continue/.env for IDE).
Researched against current Continue docs: provider: openai + custom apiBase (with
/v1), the ${{ secrets.X }} syntax, roles, and that the `cn` CLI shares the same
config. Continue's wire (/v1/chat/completions) already validated → "OK".
Tests: buildContinueModels (provider/apiBase/secret/roles, fast→autocomplete,
skip uncategorised), mergeContinueConfig (replace-ours/keep-others/defaults),
resolveContinueTarget (/v1). 6 unit tests; check:cli-i18n green.
CLI #5 of the series. `omniroute setup-kilo` configures Kilo Code
(kilocode.kilo-code, a Cline/Roo descendant) to use OmniRoute.
Two surfaces (both written, matching the dashboard cli-tools/kilo-settings):
- ~/.local/share/kilo/auth.json — CLI mode: auth["openai-compatible"] =
{ apiKey, baseUrl (WITH /v1 — Kilo appends /chat/completions), model }.
- VS Code settings.json — extension: kilocode.customProvider (name/baseURL/apiKey)
+ kilocode.defaultModel. Only touched when the file already exists.
Remote-aware (--remote/--api-key → active context → localhost). Model via --model
or an interactive pick from /v1/models (Kilo's extension has no auto-discovery).
Prints the exact UI settings to paste. Merges both files (preserves existing).
Researched against current Kilo docs: confirmed openAiBaseUrl needs /v1 (unlike
Cline's root url), the openai-compatible keys, and the export/import + CLI surfaces.
Kilo's wire (/v1/chat/completions) already validated → "OK".
Tests: buildKiloAuth (provider + /v1 + merge + key fallback), buildKiloVscodeSettings
(kilocode.* keys + preserve), resolveKiloTarget (/v1 ensure, key win). 6 unit tests;
check:cli-i18n green.
After importing a provider key, vision-capable models (OpenRouter models whose
architecture declares image input, and other synced providers) were shown as
text-only in /v1/models and the dashboard, even though image requests worked.
Root cause: SyncedAvailableModel never captured a vision flag, and the catalog's
OpenRouter live-enrichment block (which derives vision from architecture.input_modalities)
is skipped once a provider has synced models. So the synced path emitted no vision.
Fix (mirrors the existing supportsThinking capture):
- modelDiscovery.normalizeDiscoveredModels derives supportsVision via the new
detectVisionInput() from architecture.input_modalities, the string
architecture.modality ("text+image->text"), or a top-level input_modalities.
- SyncedAvailableModel gains supportsVision; the read-normalize path preserves it.
- catalog.ts emits capabilities.vision for synced models and merges (not clobbers)
capabilities when the model already exists.
TDD: tests/unit/openrouter-vision-sync-4264.test.ts — capture unit test + an
end-to-end /v1/models assertion (RED before, GREEN after).
Closes#4264
CLI #4 of the series. Cline's VS Code extension keeps config in opaque VS Code
globalStorage (not file-writable); its CLI/standalone mode reads ~/.cline/data/.
`omniroute setup-cline`:
- writes ~/.cline/data/globalState.json (act/planModeApiProvider=openai,
openAiBaseUrl = ROOT url WITHOUT /v1 — Cline appends /v1/chat/completions —
openAiModelId + planModeOpenAiModelId) and ~/.cline/data/secrets.json
(openAiApiKey), both merged to preserve existing state. Matches the dashboard
cli-tools/cline-settings schema.
- remote-aware (--remote/--api-key → active context → localhost).
- model resolved via --model or an interactive pick from /v1/models (Cline has
no model auto-discovery).
- prints the exact VS Code extension settings (Base URL/key/model) to paste,
since the extension's storage can't be written directly.
Researched against the current Cline docs (saoudrizwan.claude-dev): confirmed
the openai-compatible keys, the Plan/Act split, and that openAiBaseUrl must be
the ROOT (no /v1). Cline's wire (/v1/chat/completions) already validated → "OK".
Tests: buildClineGlobalState (provider+root+model, merge-preserve),
buildClineSecrets (key + placeholder), resolveClineTarget (/v1 strip, key win).
6 unit tests; check:cli-i18n green.
Cloudflare's /ai/models/search returns { id: "<uuid>", name: "@cf/..." } where
name is the callable slug and id is an internal UUID. The cloudflare-ai discovery
config passed the raw objects through (parseResponse: data.result), so buildResponse
used id (the UUID) as the model id — the dashboard/import listed UUIDs instead of
@cf/... model names. Map each result's name -> id (mirrors the gemini/huggingface/
clarifai parseResponse normalizers in the same map); falls through to the local
catalog on error so import never breaks.
TDD: tests/unit/cloudflare-models-uuid-4259.test.ts (RED on UUID ids -> GREEN on slugs).
Closes#4259
Applies proven patterns from the free-claude-code reference adapters:
launch (Claude Code):
- always set ANTHROPIC_AUTH_TOKEN — a no-auth sentinel when none is resolved —
so newer Claude Code doesn't stop at its local login gate before contacting
OmniRoute (an open backend ignores the value; ANTHROPIC_API_KEY stays stripped).
launch-codex:
- remote-aware: resolves the root base URL + auth from --remote/--api-key, the
active context, then localhost (was localhost/--remote only).
- inject the `omniroute` provider via `-c` flags (model_provider + base_url +
env_key + wire_api=responses + requires_openai_auth=false) so it works WITHOUT
a pre-existing ~/.codex/config.toml.
- strip OPENAI_*/CODEX_* from the child env (defense-in-depth) and set
OMNIROUTE_API_KEY to the token or a sentinel. (Honest note: this does NOT
silence codex's refresh_token log noise — that comes from ~/.codex/auth.json,
is cosmetic, and does not block requests.)
- replace a hard-coded Tailscale IP in --remote help with a placeholder.
Tests: buildCodexEnv (strip + sentinel + no-mutate), buildCodexProviderArgs
(inline provider def), resolveCodexTarget; updated buildClaudeEnv sentinel test.
Validated remotely vs VPS v3.8.30: `launch-codex --remote ... exec` → "OK".
19 unit tests pass; check:cli-i18n green.
Reconcile baseline + test drift on release/v3.8.30 (complexity, opaque surface, search count, tproxy addon). Round-robin left as a canary for the undici-dispatcher issue.
Brings Claude Code to parity with the Codex CLI integration.
- `omniroute launch` is now remote-aware: --remote <url>, --profile <name>,
--api-key. Resolves base URL + auth from the active context (so
`omniroute connect <vps>` then `omniroute launch` just works), accepts a bare
port OR a full base URL, health-checks the (possibly remote) server, and sets
CLAUDE_CONFIG_DIR for the chosen profile.
- `omniroute setup-claude` (new): fetches the live /v1/models catalog and writes
~/.claude/profiles/<name>/settings.json per model. Claude Code has no native
profile files, so CLAUDE_CONFIG_DIR is the idiomatic mechanism. Reuses the SAME
profile names as setup-codex (glm52, kimi-k27, …) via the shared categoriseModel
(now exported). The auth token is NEVER written to disk — launch injects it.
- Docs: docs/guides/CLAUDE-CODE-CONFIGURATION.md + README index. i18n (en + pt-BR).
- Tests: setup-claude profile generation (incl. "no token on disk"), buildClaudeEnv
(port + URL + CLAUDE_CONFIG_DIR), resolveLaunchTarget.
check:cli-i18n + check-docs-sync green; 13 unit tests pass.
Introduce a design system and visual identity plan to unify the OmniRoute
dashboard and marketing site. The document covers color alignment,
missing design tokens, and the implementation strategy for a consistent
brand experience.