Commit Graph

4570 Commits

Author SHA1 Message Date
Diego Rodrigues de Sa e Souza
fbb423f32b refactor(combo): ComboContext + extract phaseComboSetup (god-file split fase 1) (#4326)
Primeiro incremento da decomposição do god-file combo.ts (2604 LOC). Extrai o
bloco de setup do handleComboChat (strategy/relay/resilience/universal-handoff/
context-cache pinning/agent middleware/config cascade/timeout) para
phaseComboSetup(ctx), com ComboContext carregando o body mutável compartilhado.

- combo/context.ts: ComboContext (carrier do body mutável) + createComboContext.
- combo/comboSetup.ts: phaseComboSetup(ctx): ComboSetup (byte-equivalente ao bloco
  inline; reescreve ctx.body no pin + middleware, retorna os locals derivados).
- comboConfig.ts: resolveComboSetupConfig (DRY — encapsula o ternário do config,
  tipo único p/ ComboContext.config sem cast).
- combo.ts: -47 linhas; rebinda os locals do ctx, resto de handleComboChat intacto;
  remove 6 imports que ficaram órfãos.

Comportamento preservado: 357 testes de caracterização combo seguem verdes (+4 novos),
integração sse-correctness 5/5, typecheck 0, file-size encolhe (sem _rebaseline).
Fases 2-N = sub-planos follow-up. Ver _tasks/quality/2026-06-19-DESIGN-godfiles-decomposition.md.
2026-06-19 22:39:49 -03:00
Diego Rodrigues de Sa e Souza
5cca4ff90c fix(executors): reconstruct LMArena split auth cookie (#4271) (#4331)
LMArena migrated to @supabase/ssr chunked auth cookies: the single
arena-auth-prod-v1 cookie is now empty and the session is split across
arena-auth-prod-v1.0, .1, … (ascending). Pasting the now-empty single
cookie sent an empty session, which upstream rejected as "invalid cookie".

reconstructLMArenaCookie() rebuilds the single cookie from its chunks
(ascending join, no decode/parse — combineChunks semantics), preserving
the rest of the pasted jar; a non-empty single cookie is forwarded
unchanged (back-compat). The credential UX now instructs pasting the full
Cookie header and tracks the .0/.1 storage keys.

Closes #4271
2026-06-19 22:32:37 -03:00
Diego Rodrigues de Sa e Souza
62e0920e5e fix(compression): preserve cacheable prefix for automatic-cache providers (#3955) (#4334)
OpenAI / Codex / Azure-OpenAI use automatic prefix caching: the upstream
caches the longest matching prefix of a request (system prompt + earliest
messages) WITHOUT any explicit cache_control markers. The cache-aware
compression guard only protected that prefix when the body carried explicit
cache_control, so for automatic-cache providers the guard was skipped — and
with compression active + preserveSystemPrompt:false (or a prefix-compressing
mode) it rewrote the prefix, guaranteeing a cache miss and higher token spend
through OmniRoute than going direct.

getCacheAwareStrategy now treats isCachingProvider alone as sufficient to skip
the system prompt and downgrade aggressive/ultra (the explicit cache_control
path is a subset). openai/codex/azure are added to CACHING_PROVIDERS so they
are recognized as automatic-cache providers (this also activates the intended
prompt_cache_key cache-routing hint for OpenAI in chatCore).

Compression remains off by default — this only affects operators who enabled
it with prefix preservation turned off.

TDD: tests/unit/compression-cache-guard-3955.test.ts (RED 5/7 fail → GREEN
7/7). Aligned the existing cachingAware / strategySelector-cache-aware /
cache-control-policy / cache-control-claude-providers tests that encoded the
old (buggy) "openai is non-caching" behavior.

Refs #3955
2026-06-19 22:31:42 -03:00
Diego Rodrigues de Sa e Souza
24cee53c2f fix(executors): DuckDuckGo AI Chat uses duckduckgo.com (fixes 400) (#4037) (#4333)
The DuckDuckGo AI Chat executor fetched status/chat and set Origin/Referer
against https://duck.ai while sending Sec-Fetch-Site: same-origin, making the
same-origin triplet (host + Origin + Referer) inconsistent so the backend
rejected the request with HTTP 400. Repoint the executor's status URL, chat
URL, Origin, Referer, and warm fetch to https://duckduckgo.com (matching the
provider registry baseUrl and current DDG reverse-engineering references); the
same-origin header is now coherent.

Also relax FE_VERSION_PATTERN from a 40-hex tail to a bounded {20,40} tail so it
matches the real served x-fe-version token (20-hex, e.g.
serp_20250401_100419_ET-19d438eb199b2bf7c300) instead of silently falling back
to the hardcoded default. The bound keeps the pattern ReDoS-safe.

This is the DuckDuckGo half of the report; the separate Chipotle upstream
breakage is tracked independently.

TDD: tests/unit/duckduckgo-domain-4037.test.ts (8 assertions, RED before the
fix, GREEN after). Baseline bump 917->925 for the added comments.

Refs #4037
2026-06-19 22:30:48 -03:00
Diego Rodrigues de Sa e Souza
8f21fcee99 fix(security): bound prompt-injection regex scan to first 16KB (#3932) (#4332)
The prompt-injection guard joined every message/system string into one
buffer and ran several regexes over the whole thing on every chat
request, with no size cap. At high concurrency with large bodies (300 KB
of pasted code / RAG context) that is O(body) CPU scanning on the hot
path — a self-inflicted latency/GC source under load.

Bound both detection call sites — detectInjection() in inputSanitizer.ts
and the custom-pattern scan in promptInjection.ts — to the first 16 KB
via a named MAX_INJECTION_SCAN_BYTES constant, slicing the joined text
before the regex loop. Injection directives sit near the top of a
prompt, so the generous cap preserves real detection while scanning only
a bounded prefix. No call site removed and opt-out behavior unchanged;
this only bounds the scan length. The existing 10 MB body-size cap that
protects ingestion is separate and untouched.

TDD: tests/unit/injection-guard-scan-bound-3932.test.ts proves a
directive at the top of a >16 KB body is still detected (case 1) while
the same marker placed beyond the 16 KB cap is no longer scanned
(case 2, RED before the fix), at both call sites.

Refs #3932
2026-06-19 22:18:05 -03:00
Diego Rodrigues de Sa e Souza
2bd7fa6691 docs: ban AI-generation footers in commits/PRs (Hard Rule #16) (#4328)
Extend Hard Rule #16 beyond Co-Authored-By trailers to also forbid AI-generation
footers/descriptions (e.g. "Generated with Claude Code") anywhere in a commit
message, PR title/body, or CHANGELOG — they are equivalent to crediting an AI as
co-author. Explicitly overrides any harness/template default that auto-appends
such a footer.
2026-06-19 22:02:16 -03:00
Diego Rodrigues de Sa e Souza
1dc4e0781d chore(quality): reconcile complexity baseline 1888->1890 (lote3 merge drift) (#4330)
#4313 (harvested features) and #4323 (compression e2e audit) added new conditional
branches that landed after #4318 measured 1888; the release fast-path doesn't run
check:complexity, so reconcile on the merged release tip (Rule #9, release-volatile).
2026-06-19 21:55:39 -03:00
Diego Rodrigues de Sa e Souza
5d89fa84e7 fix(compression): end-to-end audit — fixes across the whole compression flow (#4323)
* fix(compression): SLM worker resolves deps+worker file without import.meta.url (B-SLM)

The Next.js standalone bundle (webpack) replaces createRequire(import.meta.url)
with a stub that always throws MODULE_NOT_FOUND, and freezes import.meta.url to the
build-machine path. So depsAvailable() was always false (the worker never spawned)
and resolveWorkerFile() anchored on a path absent at runtime — the SLM silently
fell back to the aggressive summarizer in production. Confirmed by inspecting
dist/.build/next/server/chunks/26410.js (stub module 215743 + frozen file:// path).

Replace both with filesystem probing from runtime anchors (process.cwd(),
process.argv[1]) that survive the bundle. Necessary complement to #4286 (deps
co-location) for the SLM to actually engage in prod; still fail-open without it.
VPS live validation deferred (Rule #18); local resolver regression tests added.

* fix(compression): ultra heuristic preserves code blocks / inline code / URLs (B-ULTRA-CODE)

ultra.ts called pruneByScore on raw text with no tombstoning, so the token pruner
dropped low-score code tokens (`b)`, `{`, `+`) inside fenced blocks while leaving the
fence markers intact — output that looked like valid code but was syntactically
destroyed. caveman + llmlingua both extract/restore preserved blocks first; ultra was
the only pruning engine that didn't.

Add pruneProseOnly(): extractPreservedBlocks tombstones fenced code, inline code,
URLs, CONST_CASE, versions; only the prose between placeholders is pruned; preserved
blocks are re-stitched verbatim.

* fix(compression): GCF round-trips values containing the inline-array pattern [..]: (B-GCF-QUOTE)

A value like `ERR[404]: Not Found` / `[Speaker 1]: Hello` nested one level deep was
emitted bare and re-parsed by the decoder as an inline-array header → it threw
`count_mismatch` (or silently decoded wrong), losing the whole block. headroomEngine
.apply() ships such blobs in prod, so this was a reachable lossless violation.

Two complementary fixes, both per SPEC §2.4:
- encode: needsQuote() now quotes strings matching `[`…`]``:` (spec compliance / other
  decoders).
- decode: the inline-array branch only fires when the bracket is in the KEY position
  (no `=` before it), so a quoted `note="ERR[404]: …"` value falls through to key=value.

* fix(compression): aggressive fidelity — keep text blocks, compress Anthropic tool_result, don't corrupt JSON (B-AGG-*)

Three fidelity fixes in the aggressive path (each TDD, aggressive-fidelity.test.ts):
- B-AGG-TEXTDROP: replaceTextContent dropped 2nd+ text blocks unconditionally; now a
  trailing block is dropped only when its text is already subsumed by newText, else kept.
- B-AGG-ANTHROPIC-TR: tool-result compression only fired for OpenAI role:tool messages;
  now Anthropic-shape tool_result content blocks (inside user messages) are compressed
  too, preserving tool_use_id + block structure.
- B-AGG-JSONTAG: the [COMPRESSED:aging:*] prefix corrupted JSON/code payloads; pure JSON
  is now kept verbatim+untagged (stays parseable), fenced blocks get the tag on a
  preceding line.

* fix(compression): accessibility collapse preserves [ref] anchors + fires on interleaved trees (B-MCPA11Y-*)

- B-MCPA11Y-ANCHORS: collapseRepeated silently dropped the omitted middle siblings'
  [ref=eNN] anchors (the agent could no longer click them); now every omitted ref is
  kept alongside the collapse notice. Wires the previously-dead preserveRefPattern.
  Invariant: extractRefs(input) ⊆ extractRefs(output).
- B-MCPA11Y-COLLAPSE: noise removal blanked lines (replace→""), and a blank line broke
  the sibling run so collapse never fired on realistic interleaved trees; noise lines
  are now deleted, and the sibling walk skips stray blanks.

* fix(compression): rtk intensity scales the line budget (B-RTK-INTENSITY)

The intensity knob only set smartTruncate's preserveHead/Tail (16↔24), which rarely
fired because the matched filter capped lines first — so minimal/standard/aggressive
produced byte-identical output on filter-matched tool output. effectiveMaxLines() now
scales the effective line budget (minimal 1.5x, standard 1x, aggressive 0.5x) at both
the per-filter and engine-level truncation sites. Both go through smartTruncate with
priorityPatterns, so error/failure lines survive at every intensity (tested).

* fix(compression): robust language detection + auto-detect honors the detected pack (B-LANG-*)

- B-LANG-DETECTOR: detector was first-match-wins on a single keyword, and some hints are
  English-ambiguous ("configuration" in fr, "error" in es) → English text misclassified.
  Now score-based (count native-keyword hits, highest wins), and the two English-ambiguous
  words are removed from the hint lists, so a lone shared word never misclassifies while
  sparse-keyword languages (id) still detect on a single native word.
- B-LANG-DORMANT: with autoDetectLanguage on but enabledPacks ["en"], detected non-English
  text fell back to the English pack, whose `articles` rule deletes foreign articles
  (pt-BR "a"/"o"). Auto-detect now uses the detected pack directly (it always has rules);
  enabledPacks still gates manual selection.

* fix(compression): mode selection enables its engine + align stacked allowlist (B-MODE-ENGINE-DECOUPLE, B-PIPELINE-DIVERGENCE)

- B-MODE-ENGINE-DECOUPLE: picking the standard/rtk MODE now runs caveman/rtk regardless of
  the per-engine enabled flag — the mode selection is the enable signal (the per-engine flag
  still gates stacked pipeline steps). Previously an operator who picked a mode but left the
  engine toggle off got silent 0% compression.
- B-PIPELINE-DIVERGENCE: the global stackedPipeline normalizer stripped
  session-dedup/ccr/headroom/llmlingua (engines the combo path accepts via KNOWN_ENGINE_IDS).
  The allowlist now matches, so the global setting can use all registered engines.

* docs(compression): correct SLM "stable" claim + document partial packs / stacked telemetry limits

- The llmlingua `stable:true` comment claimed the bundle walk-up + deps-gate were
  "confirmed against the live install" — that was wrong (webpack froze import.meta.url and
  stubbed createRequire, so the worker never spawned in prod). Corrected to reflect B-SLM.
- COMPRESSION_ENGINES.md: add a Known limitations section (SLM dep co-location requirement,
  partial de/fr/ja packs, no-op engines absent from engineBreakdown).

* fix(compression): cast normalized engine id to CompressionPipelineStep['engine'] (typecheck)
2026-06-19 21:51:12 -03:00
Diego Rodrigues de Sa e Souza
2c0fd04704 feat: implement 5 harvested feature requests (#4239, #4155, #3841, #3266, #4240) (#4313)
* 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.
2026-06-19 21:49:27 -03:00
Diego Rodrigues de Sa e Souza
7ce875f404 feat(providers): provider model sweep — live discovery, refreshed catalogs, dead-provider cleanup (#4324)
* 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).
2026-06-19 21:45:54 -03:00
Diego Rodrigues de Sa e Souza
831bd0a7b3 feat(quality): make the a11y gate real (@axe-core/playwright in nightly) (#4321)
* 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.
2026-06-19 21:43:51 -03:00
Diego Rodrigues de Sa e Souza
facbb1964f feat(quality): unblock R1 — test-redundancy measurement via disableBail (#4322)
* 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).
2026-06-19 21:40:57 -03:00
Diego Rodrigues de Sa e Souza
0d6c5686d2 feat(dashboard): list MITM hosts-file entries in the tool card (#4325)
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>
2026-06-19 21:39:56 -03:00
Ardem2025
5c68048650 fix(sse): release reader and cancel stream on abort/error to prevent Undici pool socket leak (#4309)
* 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>
2026-06-19 21:39:28 -03:00
NOXX - Commiter
3b21a952fc fix(kiro): emit early role-only start chunk to release stream-readiness gate (#4311)
* 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>
2026-06-19 21:36:12 -03:00
Diego Rodrigues de Sa e Souza
ad4338449c fix(quality): complexity gate covers bin/+electron + tracked-artifacts in pre-commit (#4318)
* 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.
2026-06-19 21:34:59 -03:00
Diego Rodrigues de Sa e Souza
0abc00cf2a fix(translator): clamp Responses API call_id to 64 chars (#4317)
* 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>
2026-06-19 21:33:28 -03:00
Diego Rodrigues de Sa e Souza
032387401a fix(oauth): GitHub Copilot token refresh sends the public client_id (#4320)
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>
2026-06-19 21:31:42 -03:00
Diego Rodrigues de Sa e Souza
5193a595bf fix(dashboard): proxy modal stops pre-filling new scopes with an unrelated proxy (#4312)
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.
2026-06-19 21:31:39 -03:00
Diego Rodrigues de Sa e Souza
466d3cf6eb fix(open-sse): inner-ai stops silently rerouting unmatched models to models[0] (#4310)
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).
2026-06-19 21:31:30 -03:00
Diego Rodrigues de Sa e Souza
6912664133 fix(sse): retry direct socket failures on a fresh no-keep-alive dispatcher (#4252) (#4319)
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
2026-06-19 20:44:07 -03:00
Diego Rodrigues de Sa e Souza
9a678497ad fix(sse): stop combo at the first body-specific 400 (#4279) (#4316)
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
2026-06-19 20:26:03 -03:00
Paijo
db7c8c5edc fix(pollinations): handle auth-required premium models (#4266)
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.
2026-06-19 20:23:05 -03:00
Diego Rodrigues de Sa e Souza
84bf5dc7de fix(sse): recover reconstructed message when Responses terminal output is textless (#3948) (#4315)
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
2026-06-19 20:17:49 -03:00
Diego Rodrigues de Sa e Souza
6103288c48 fix(executors): preserve tool-name casing on native Claude OAuth (#4307) (#4314)
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
2026-06-19 20:12:03 -03:00
Diego Rodrigues de Sa e Souza
a23d0d678a fix(api): semantic-cache HIT bills incremental cost 0 + X-OmniRoute-Cost-Saved (PRD-2026-06-19) (#4308)
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.
2026-06-19 18:50:08 -03:00
Diego Rodrigues de Sa e Souza
205be2f8f8 fix(deps): bump undici to 7.28.0 and dompurify to 3.4.11 (security) (#4306)
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.
2026-06-19 18:28:08 -03:00
Diego Rodrigues de Sa e Souza
bbc9d1e1c5 feat(quality): seed per-module mutationScore floors + blocking aggregation ratchet (T3) (#4305)
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.
2026-06-19 18:28:06 -03:00
Diego Rodrigues de Sa e Souza
23455fdb0a feat(cli): setup-gemini — point the Gemini CLI at OmniRoute's native /v1beta endpoint (#4303)
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).
2026-06-19 18:14:58 -03:00
Diego Rodrigues de Sa e Souza
0ab1876008 feat(mitm): translate Antigravity cloudcode end-to-end (Gap B) (#4299)
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).
2026-06-19 18:01:14 -03:00
Xiangzhe
915991c762 fix(codex): isolate Spark quota scope (#4293)
* fix(codex): isolate Spark quota scope

* fix(codex): address Spark quota review feedback

* fix(ci): update Electron undici override

* fix(ci): update root undici overrides

* test(integration): sync stale expectations

* test(tproxy): tolerate available native addon

* test(tproxy): avoid environment-specific skips

* test(tproxy): keep assertion count stable

* fix(ci): stabilize quality and tproxy checks

* chore(ci): rebaseline auth file size

* fix(ci): extend node compatibility budget

* chore(quality): reconcile complexity + file-size baselines after release/v3.8.30 merge (#4293)

Measured on the actual merged tree (not the PR's main-based estimate):
complexity 1885->1887 (+2); file-size auth.ts 2219->2279, chatCore.ts 5116->5125,
accountFallback.ts 1727->1731, + the 4 Codex test files. Drift test-file conflicts
(search-providers-catalog, tproxy-transparent-socket, integration-wiring) resolved
to the already-merged release versions (#4276).

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>

---------

Co-authored-by: ci <ci@local>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-06-19 17:57:23 -03:00
Diego Rodrigues de Sa e Souza
165d9cdae9 feat(cli): setup-aider — configure Aider for OmniRoute (.aider.conf.yml + env recipe) (#4302)
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.
2026-06-19 17:49:47 -03:00
Édrick Renan
b01b72052f fix(dashboard): improve API try it functionality (#4296)
* 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>
2026-06-19 17:48:54 -03:00
PizzaV
b56b7b1914 fix: polyfill crypto.randomUUID for non-secure contexts (#4287)
* 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>
2026-06-19 17:45:46 -03:00
dependabot[bot]
fcdf29f8c5 chore(deps): bump actions/checkout from 4 to 7 (#4297)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Commits](https://github.com/actions/checkout/compare/v4...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-19 17:39:59 -03:00
Wilson
133432b523 fix(proxy): allow concurrent proxy dispatcher streams (#4288) 2026-06-19 17:39:57 -03:00
Diego Rodrigues de Sa e Souza
0acb8d0aeb fix(build): co-locate llmlingua SLM optionals into dist/node_modules (postinstall) (#4286)
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
2026-06-19 17:39:10 -03:00
Diego Rodrigues de Sa e Souza
efbe0a6af1 fix(mitm): surface AgentBridge traffic in the Traffic Inspector (D4 ingest) (#4285)
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).
2026-06-19 17:39:08 -03:00
Diego Rodrigues de Sa e Souza
0ec476b755 feat(cli): setup-qwen — configure Qwen Code for OmniRoute (settings.json modelProvider) (#4301)
CLI #11 of the series. `omniroute setup-qwen` writes Qwen Code's file-based
~/.qwen/settings.json: an openai `modelProvider` (id omniroute, authType openai,
baseUrl WITH /v1, envKey OMNIROUTE_API_KEY — secret stays in the env), selects it,
sets the model. Merges (de-dupes the omniroute provider, preserves the rest).
Remote-aware; model via --model or interactive pick; headless test `qwen -p`.

Researched against QwenLM/qwen-code: modelProviders authType openai, baseUrl /v1,
envKey reference. Qwen's wire (/v1/chat/completions) already validated → "OK".

Tests: resolveQwenTarget (/v1, key), buildQwenSettings (openai provider + /v1 +
envKey + model, de-dupe + preserve). 4 unit tests; cli-i18n green.
2026-06-19 17:26:38 -03:00
Diego Rodrigues de Sa e Souza
25f9dac9e9 feat(cli): setup-goose — configure Goose for OmniRoute (config.yaml + env recipe) (#4300)
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.
2026-06-19 17:04:44 -03:00
Diego Rodrigues de Sa e Souza
26dd5d775c feat(cli): setup-crush — OmniRoute openai-compat provider in crush.json (#4298)
CLI #9 of the series. `omniroute setup-crush` writes Crush's file-based
~/.config/crush/crush.json with an `openai-compat` provider for OmniRoute:
base_url WITH /v1, api_key referenced as $OMNIROUTE_API_KEY (secret off disk),
curated catalog models with context_window. Merges (preserves existing config).
Remote-aware (--remote/--api-key → active context → localhost); --only filter.

Researched against charmbracelet/crush: openai-compat provider type, base_url
needs /v1, $VAR api_key references. Crush's wire (/v1/chat/completions) already
validated → "OK".

Tests: resolveCrushTarget (/v1, key), buildCrushProvider (openai-compat + env-ref
+ curated models + skip-unknown), mergeCrushConfig (preserve). 4 unit tests; cli-i18n green.
2026-06-19 16:38:48 -03:00
Diego Rodrigues de Sa e Souza
bf5b615969 feat(memory): x-omniroute-no-memory opt-out + memory off-by-default + token-cost alert (PRD-2026-06-19) (#4290)
* 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.
2026-06-19 16:14:18 -03:00
Diego Rodrigues de Sa e Souza
5b40069b71 feat(cli): setup-roo — configure Roo Code for OmniRoute (import JSON + autoImport + UI) (#4292)
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.
2026-06-19 16:13:26 -03:00
Diego Rodrigues de Sa e Souza
9616b65b53 feat(cli): setup-cursor — print Cursor setup steps for OmniRoute (#4291)
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.
2026-06-19 13:21:55 -03:00
Diego Rodrigues de Sa e Souza
138ea6628d feat(cli): setup-continue — generate ~/.continue/config.yaml for OmniRoute (#4289)
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.
2026-06-19 13:19:12 -03:00
Diego Rodrigues de Sa e Souza
70bd6fbcc9 feat(cli): setup-kilo — configure Kilo Code for OmniRoute (CLI auth + VS Code settings) (#4284)
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.
2026-06-19 12:57:22 -03:00
Diego Rodrigues de Sa e Souza
6f16faa039 fix(models): keep vision capability for imported (synced) models (#4264) (#4283)
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
2026-06-19 12:53:06 -03:00
Diego Rodrigues de Sa e Souza
158a2246dd feat(cli): setup-cline — configure Cline for OmniRoute (CLI files + VS Code hints) (#4280)
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.
2026-06-19 12:30:51 -03:00
Diego Rodrigues de Sa e Souza
550440f65f fix(providers): Cloudflare Workers AI discovery uses model names, not UUIDs (#4259) (#4282)
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
2026-06-19 12:25:35 -03:00
Diego Rodrigues de Sa e Souza
98b0d5e51e fix(sse): surface undici err.cause on dispatcher failure (#4281)
Surface err.cause + propagate diagnosable error fast on undici dispatcher failure. TDD. #4252.
2026-06-19 12:10:43 -03:00