Compare commits

...

421 Commits

Author SHA1 Message Date
Diego Rodrigues de Sa e Souza
57b1b66fb2 chore(release): open v3.8.48 development cycle 2026-07-13 01:54:25 -03:00
Diego Rodrigues de Sa e Souza
bedd8bcc38 chore(quality): v3.8.47 cycle-close pct rebaselines (openapiCoverage 39.3->38, i18nUiCoverage 76.8->75.5) with justification 2026-07-13 01:15:12 -03:00
Diego Rodrigues de Sa e Souza
f11ec808fb fix(quality): restore zizmorFindings ratchet object shape (value 169 + justification) 2026-07-13 01:12:26 -03:00
Diego Rodrigues de Sa e Souza
bf293a9794 chore(release): v3.8.47 pre-flight fixes — orphan test relocation (#6943), eslint suppression match, file-size/zizmor rebaselines 2026-07-13 01:04:46 -03:00
Diego Rodrigues de Sa e Souza
4478f5f71d chore(base): backfill #6909 i18n keys (en+pt-BR) and align gemini defaults test with #6943 (unit-full pre-flight) 2026-07-13 00:28:12 -03:00
Diego Rodrigues de Sa e Souza
1289757704 fix(test): deterministic openadapter live-catalog import repro (#6967) 2026-07-12 20:54:15 -03:00
Diego Rodrigues de Sa e Souza
f34ddb0cf2 fix(test): align emergency fallback budget-exhaustion test with #6912 max_tokens normalization (#6967)
The test asserted both max_tokens and max_completion_tokens=4096 on the
nvidia/openai/gpt-oss-120b emergency fallback request. Commit a34fb6b3e
(#6912, merged into this release tip) added a symmetric normalization in
chatCore.ts that renames/deletes the redundant max_completion_tokens field
whenever the target provider supportsMaxTokens() (nvidia does), so only
max_tokens reaches the upstream request. The old dual-field assertion is
an outdated contract, not a regression. Align the test to the new
intentional behavior while keeping the max_tokens=4096 cap assertion as
the fallback-cap guard.
2026-07-12 20:46:23 -03:00
Diego Rodrigues de Sa e Souza
7a5b51be68 chore(base): fix release-tip base-reds — eslint severity revert (#6786 regression), migration gap 121, file-size freeze bumps 2026-07-12 20:40:35 -03:00
Diego Rodrigues de Sa e Souza
e078664ddd fix(sse): schema-aware optional tool-arg normalization for Codex routes (#6951) (#6992)
stripEmptyOptionalToolArgs was allowlist-only (Read/Subagent) and only
stripped empty-string/empty-array values, so Responses API strict mode
(every property forced into `required`) could forward a forced non-empty
value (e.g. Agent.isolation) or a schema-declared default value verbatim
to the client. Add schema-aware drop-if-default and generalized
drop-if-empty (any tool, gated on schema.required), and thread each
tool's JSON Schema from the request's tools[] into the two streaming
call sites (response.output_item.done handling).

Closes #6951
2026-07-12 18:08:07 -03:00
Diego Rodrigues de Sa e Souza
9b43a00b60 fix(combos): show embedding/rerank models and disambiguate duplicate names in builder options (#6975, #6957) (#6991)
Removes the leftover chat-only isChatCapable gate from addModelOption() (#6975) and adds a name-disambiguation pass at the end of buildModelOptions() so distinct model ids sharing the same upstream display name fall back to their id (#6957). Both proven with TDD repro tests (RED->GREEN).
2026-07-12 18:07:58 -03:00
Diego Rodrigues de Sa e Souza
94328d5cb2 fix(sse): apply commentary-phase drop filter in TRANSLATE mode (#6952) (#6990)
The #6199/#6561 commentary-phase filter (shouldDropResponsesCommentaryEvent)
was wired only into createSSEStream's PASSTHROUGH branch. The TRANSLATE-mode
loop (openai-responses upstream -> another client format, e.g. codex routes
streaming into Claude Code) called translateResponse() on every raw chunk
without checking phase, so internal commentary-phase scratchpad text leaked
into the client-visible content channel as duplicate prose and narrated
tool-call arguments.

Extends the same stateful filter into TRANSLATE mode via a small factory
(createTranslateCommentaryFilter) that owns its own item/index Sets, keeping
the wiring in stream.ts (a frozen file) to a single guarded line.

Fail->pass evidence:
- tests/unit/repro-6952-commentary.test.ts against origin/HEAD (pre-fix):
  FAILED - "commentary-phase prose must not reach the translated client stream"
- Same test against the fix: PASSED (2/2)
2026-07-12 18:07:49 -03:00
Diego Rodrigues de Sa e Souza
271bf52da1 chore(base): fix 2 mechanical release-tip base-reds (relayProbeStats re-export + OMNI_MAX_CONCURRENT_CONNECTIONS docs) 2026-07-12 18:07:20 -03:00
Paijo
b4c47f5cbf feat(sse): add connection backpressure for chat handler (#6590)
Add checkConnectionCapacity guard with 429 + Retry-After in handleChat().
Introduce OMNI_MAX_CONCURRENT_CONNECTIONS env-bound cap, disabled (0) by
default so existing deployments are unaffected until an operator opts in.

Reconstructed from PR #6590, isolating only the backpressure change —
the original branch also carried unrelated headroom/docker/perf work
from the author's separate #6572 branch.

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-12 10:50:27 -03:00
Rafael Dias Zendron
38d6cd9955 fix(6813): fix thinking budget zero drop and default thinkingConfig injection (#6943)
* fix(electron): bump electron 42→43 + build better-sqlite3 from source (ABI 148) (#6605)

fix(electron): bump electron 42→43 + rebuild better-sqlite3 from source against the Electron ABI (148).

Electron 43 raises NODE_MODULE_VERSION to 148; better-sqlite3@12.11.1 has no electron-v148 prebuild, so the packaged app died with 'Nenhum driver SQLite disponível'. prepare-electron-standalone now compiles better-sqlite3 from source against the electron headers into build/Release (where 'bindings' resolves it). Validated by Electron Package Smoke (green) + local (node_register_module_v148).

Supersedes #6378. (--admin: the only reds are SonarQube/SonarCloud failing on a coverage-report artifact digest-mismatch — a GitHub Actions infra flake, not this diff; Sonar is green on main and the diff touches only the electron build.)

* deps: bump the development group across 1 directory with 6 updates (#6588)

deps: bump the development group (6 updates). Rebased onto current main; all checks green after the electron-smoke fix (#6605).

* fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump (#6620)

fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump.

undici 8.6+ changed ProxyAgent to forward plain-HTTP via request-proxy instead of CONNECT, breaking OAuth refresh through a connection proxy (501). proxyDispatcher now passes proxyTunnel:true. Validated: Unit Tests 3/8 (the OAuth-proxy test) green, new regression test green (fails without the fix on undici 8.7), SonarQube green.

Supersedes #6380. (--admin: the only red is Electron Package Smoke failing on a next-build artifact 'digest-mismatch' — a GitHub Actions infra flake corrupting the asar ('file data stream has unexpected number of bytes'); the better-sqlite3 rebuild itself succeeded (gyp ok) and the electron path is unchanged from #6605 which passed the smoke. Not this diff.)

* fix(6813): fix thinking budget zero drop and default thinkingConfig injection

- Fix truthy check for budget_tokens to allow 0
- Stop injecting default thinkingConfig when no knobs present
- Add tests covering all scenarios

Related: #6813

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-12 10:49:07 -03:00
Saren
d2d4cf0e17 fix(sse): normalize assistant input_text to output_text in Codex Responses input (#6932)
codex-cli replays assistant history with content parts typed as
`input_text`, but the Responses API only accepts `output_text`
(or `refusal`) on assistant turns — `input_text` is user-only.
`normalizeCodexMessageContentPart` previously only rewrote parts
literally typed `text`, leaving explicit `input_text` on assistant
turns untouched, which the Codex/OpenAI backend rejects with a 400.

Rewrite explicit `input_text` (and `text`) to `output_text` on
assistant-role parts, dropping the assistant-only `annotations`,
`logprobs`, and `obfuscation` fields. Mode-agnostic, applies to
all Codex models.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-12 10:47:51 -03:00
Paijo
a61a4dc673 fix: eliminate redundant getApiKeyMetadata call in embeddings route (#6929)
enforceApiKeyPolicy() already fetches the API key metadata and returns
it as policy.apiKeyInfo. The old code at line 72 called getApiKeyMetadata
a third time per request (third hash+DB query after isValidApiKey and
enforceApiKeyPolicy's internal fetch).

Change: use policy.apiKeyInfo directly instead of re-querying. Also
removes the now-unused getApiKeyMetadata import.

Adds a regression test exercising the dashboard-playground-key path
(no bearer token, only enforceApiKeyPolicy's resolvePlaygroundTestKey
fallback resolves the key) — the old apiKeyRaw-gated call always
produced a null apiKeyMeta on that path, while policy.apiKeyInfo
correctly carries it through to the downstream call log.

Split out of the original PR: dropped the unrelated 46-provider-icon
commit that had been bundled onto the same branch.

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-12 10:33:33 -03:00
Paijo
66cb93f9bd perf: thread pre-fetched token to checkRateLimit avoiding re-query (#6930)
* perf: thread pre-fetched token to checkRateLimit avoiding re-query

getRelayTokenByHash already fetches the full RelayToken row. A few
lines later checkRateLimit(token.id) does a second SELECT * FROM
relay_tokens on a different predicate (id instead of token_hash).

Change:
- checkRateLimit accepts an optional existingToken parameter; when
  provided, skips the re-query entirely.
- Both relay routes (chat completions + bifrost) pass the already-
  fetched token.
- The function now uses RelayToken (camelCase) instead of RelayTokenRow
  (snake_case) when the token is passed in.

PR-URL: fix-relay-thread-token

* test(db): add regression coverage for checkRateLimit existingToken fast-path

Adds node:test coverage for src/lib/db/relayProxies.ts::checkRateLimit
proving the existingToken fast-path (pre-fetched RelayToken threaded in,
no re-query) agrees with the legacy re-query path (no token passed),
and that the per-minute cap is still enforced through the fast-path.
Also adds a changelog.d fragment for the perf fix in 9d4cd90e7.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-12 10:30:26 -03:00
Paijo
084fca42bf fix(tokenHealthCheck): case-sensitive provider comparisons break rotating/gh checks (#6947)
ROTATING_REFRESH_PROVIDERS.has(conn.provider) fails for 'OpenAI' or 'Github'
- the set is all lowercase. Same issue for the GitHub Copilot sub-token
refresh guard. Both now normalize to lowercase before comparison, matching
the established pattern from getHealthCheckSkipProviders() (line 201) and
isGitHubAccessTokenOnlyConnection() (line 94).

Replaces the whole-file regex assertion in oauth-providers-error-handling
(which passed even on unfixed code) with two statement-scoped regression
tests that fail against origin/release/v3.8.47's unfixed source and pass
only once both call sites are normalized.

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-12 10:29:48 -03:00
Paijo
19d1a5d391 fix(db): add authType filter support to getProviderConnections (#6946)
getProviderConnections ignored the authType query param, causing
callers like tokenHealthCheck.ts and /api/token-health to fetch and
decrypt every connection instead of only the OAuth ones they asked
for. Add the missing auth_type WHERE clause and a regression test.

Rebased to drop the unrelated 46-icon commit (duplicate of #6926)
and the accidentally-committed tests/unit/authz/__stub_apiKeys.mjs
runtime artifact; replaced with a real unit test asserting the
authType filter excludes non-matching connections.

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-12 10:27:42 -03:00
Diego Rodrigues de Sa e Souza
2a5f9a5ed7 fix(sse): flatten structured (array) content in Qwen Web executor (#6927)
* fix(sse): flatten structured (array) content in Qwen Web executor

foldMessages did String(m.content), turning OpenAI-style content-part arrays
into the literal "[object Object]" prompt. Add contentToText() to extract the
text parts. Reported on the support mesh.

TDD: red->green regression test tests/unit/qwen-web-content-array-serialization.test.ts

* docs(changelog): add fragment for #6927

* fix(stryker): register qwen-web content-array test in tap.testFiles

Fast Quality Gates flagged the new coverage for open-sse/executors/qwen-web.ts
as missing from stryker.conf.json's tap.testFiles list.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-12 10:26:58 -03:00
brick30llc-ctrl
7ca422d258 fix(sse): escape backslash in ChatGPT-web citation link text (#6569) (#6944)
* fix(sse): escape backslash in ChatGPT-web citation link text (#6569)

markdownLinkText() escaped [ and ] but not the backslash itself, so a
citation label ending in (or containing) a backslash produced a broken
Markdown link — e.g. [Path C:\](url), where the trailing \ escapes the
closing bracket and consumes the link. Escape the backslash first, then
the brackets.

Clears the CodeQL js/incomplete-sanitization alerts at
open-sse/executors/chatgpt-web/citations.ts:52 (2 of the 9 new alerts
on the v3.8.47 release PR).

Regression guard: tests/unit/chatgpt-web-citations-escape.test.ts
(trailing backslash, backslash-before-bracket, bracket-only, plain).

* chore(changelog): add changelog.d fragment for #6944

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: brick30llc-ctrl <admin@brick30.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-12 10:26:21 -03:00
Diego Rodrigues de Sa e Souza
7092a1a62b fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) (#6959)
cloakAntigravityToolPayload() injects decoy functionDeclarations
(search_web, browser_subagent, read_url_content, generate_image) that
mimic Antigravity's built-in server-side agent tools whenever any real
tool is declared, but never set the companion
toolConfig.includeServerSideToolInvocations flag a genuine Antigravity
client sends alongside them. Google's Cloud Code backend (Gemini 3+)
requires this opt-in whenever server-side built-in tool categories are
combined with custom function declarations, causing every Antigravity
tool-calling request to fail upstream.

Set toolConfig.includeServerSideToolInvocations = true whenever decoy
tools are injected.
2026-07-12 10:24:08 -03:00
Diego Rodrigues de Sa e Souza
c8f116d806 fix(api): use local-first SSRF guard for LAN model-list discovery (#6939) (#6966) 2026-07-12 10:24:01 -03:00
Diego Rodrigues de Sa e Souza
c1fc661c90 fix(sse): classify LAN embeddings providers as no-auth (#6925) (#6962)
Private/LAN embeddings provider_nodes (10.0.0.0/8, 192.168.0.0/16,
100.64.0.0/10 CGNAT) were excluded by a hand-rolled hostname filter that
only matched localhost/127.0.0.1/172.16-31, forcing them through the
apikey/bearer credential fallback and returning 401 for keyless local
providers like a LAN Ollama instance.

Reuse the shared isPrivateHost()/isCloudMetadataHost() classification from
outboundUrlGuard.ts in both the dynamic-provider filter and the
provider_node fallback branch, so any private host resolves to
authType 'none' while cloud-metadata endpoints stay blocked.
2026-07-12 10:23:54 -03:00
Diego Rodrigues de Sa e Souza
152d77cf4b fix(dashboard): label audio/embeddings/image compatible providers by kind on ProviderCard (#6936) (#6961)
ProviderCard's compatibility badge used a binary apiType ternary
(responses vs everything-else -> "Chat"), so audio-transcriptions,
audio-speech, images-generations and embeddings compatible providers
(e.g. a locally-hosted speaches TTS/STT server) were mislabeled as
"Chat". Reuse the existing KIND_LABEL map (stt/tts/image/embedding)
instead of adding new i18n keys.
2026-07-12 10:23:47 -03:00
Diego Rodrigues de Sa e Souza
86af4765f5 fix(sse): omit removed attachments field from Muse Spark Web request (#6935) (#6960) 2026-07-12 10:23:40 -03:00
Diego Rodrigues de Sa e Souza
45d38bc4b2 fix(sse): defer response.completed until trailing usage-only chunk (#6906) (#6965)
Real OpenAI-compatible upstreams with stream_options.include_usage=true
send finish_reason in one chunk (usage: null) and the actual token counts
in a separate, trailing usage-only chunk (choices: [], usage: {...}).
Both the live translator (openai-responses.ts) and the legacy transformer
(responsesTransformer.ts) fired response.completed as soon as they saw
finish_reason, so the trailing usage chunk's token counts were captured
into state but never emitted -- Codex CLI and other /v1/responses
consumers saw response.completed with no usage field (permanent 0%
context-used).

Both translators now defer response.completed via an
awaitingTrailingUsage state flag when finish_reason arrives without
usage already captured, and complete on the next usage-only chunk (or
at stream end via the existing flush fallback) instead. Extracted the
duplicated events/emit boilerplate into a new
openai-responses/eventEmitter.ts leaf to keep the frozen
openai-responses.ts file under its file-size baseline.

Fixes 3 existing tests that encoded the old chunk ordering and adds a
permanent regression test (tests/unit/responses-usage-trailing-6906.ts)
covering both translators.
2026-07-12 10:23:33 -03:00
Diego Rodrigues de Sa e Souza
704a7e8947 fix(sse): rename max_completion_tokens to max_tokens for volcengine/DeepSeek (#6912) (#6964) 2026-07-12 10:23:26 -03:00
Diego Rodrigues de Sa e Souza
0d832f7d79 fix(sse): wire shared quota-fetch throttle into all provider fetchers (#6911) (#6963)
OMNIROUTE_QUOTA_FETCH_MIN_INTERVAL_MS / quotaFetchThrottle.ts documents
itself as used by 'the provider quota fetchers' (plural), but only
codexQuotaFetcher.ts ever called throttleQuotaFetch(). N accounts on
one IP for DeepSeek, Bailian, OpenCode, or Crof still burst
simultaneously.

Wire throttleQuotaFetch() into fetchDeepseekQuota, fetchBailianQuota
(both the primary and China-region retry fetch sites),
fetchOpencodeQuota, and fetchCrofUsage, placed after the existing
cache short-circuit so cache hits stay unaffected (mirrors the
codexQuotaFetcher.ts pattern).

PROVIDER_LIMITS_SYNC_SPACING_MS / providerLimits.ts's OAuth vs
non-OAuth split is left unchanged — that split is intentional by
design and already regression-guarded by
tests/unit/provider-limits-oauth-sequential-sync.test.ts.

The generic usage.ts::getUsageForProvider dispatch path (github,
glm, minimax, nanogpt, xai, etc.) is intentionally out of scope for
this fix to avoid scope creep; ENVIRONMENT.md now documents the
actual post-fix coverage instead of the prior overclaim.
2026-07-12 10:23:19 -03:00
Paijo
8017f4127f feat: add icons for 46 missing provider images (#6926)
* feat: add icons for 46 missing provider images

- Add SVG icons for 46 providers missing brand images
- Add 3 LOBE aliases (bai, clinepass, copilot-m365-web)
- Register all new SVGs in KNOWN_SVGS lookup

New SVG icons cover: api-airforce, auggie, bluesminds, byteplus,
bytez, charm-hyper, chipotle, chutes, crof, dgrid, digitalocean,
dit, duckduckgo-web, factory, freeaiapikey, freemodel-dev, galadriel,
gitlawb, gitlawb-gmi, hackclub, haiper, hcnsec, ideogram, kenari,
leonardo, llm7, modelscope, nube, openadapter, orcarouter, pioneer,
publicai, qiniu, requesty, sumopod, t3-web, theoldllm, tokenrouter,
uncloseai, veoaifree-web, wafer, x5lab, yuanbao-web, zed-hosted,
zenmux, zenmux-free

* fix(icons): restore accidentally-deleted cohere alias in LOBE_PROVIDER_ALIASES

The 46-icon addition dropped the existing `cohere: "Cohere"` entry; restore it
alphabetically between codex-cloud and comfyui.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-12 10:22:04 -03:00
Paijo
10cb2447b1 [trim] feat(combo): add context requirements config for target filtering (#6907)
* feat(combo): add context requirements config for target filtering

Add contextRequirements config field to combo runtime config:
- minContextWindow: filter models below threshold (0-10M tokens)
- preferLargeContext: sort targets by context size descending
- contextFilterMode: 'strict' excludes unknown limits, 'lenient' includes them

Implementation:
- Added Zod schema validation in combo.ts
- Created contextRequirements.ts module with applyContextRequirements()
- Integrated filtering after filterTargetsByRequestCompatibility()
- Full test coverage with unit + integration tests

Tests: 17/17 pass (combo-context-requirements.test.ts + integration)

* feat(combo): add ContextRequirementsEditor UI component

Add standalone React component for editing context requirements config:
- Slider for minContextWindow (0 to 1M tokens) with presets
- Toggle for preferLargeContext sorting
- Radio group for contextFilterMode (strict/lenient)
- Tooltips explaining each option
- Active filters summary display

Component features:
- Shadcn UI components (Card, Slider, Switch, RadioGroup)
- Preset buttons for common context sizes (8K, 32K, 128K, 1M)
- Conditional display of filter mode when minContextWindow > 0
- Clear visual feedback of active filters

Integration:
Import and use in combo config form where other config fields
like fusionTuning and judgeModel are edited. Pass combo.config.contextRequirements
as value prop and update on onChange.

Example usage:
<ContextRequirementsEditor
  value={config.contextRequirements}
  onChange={(val) => updateConfig({ contextRequirements: val })}
/>

UI matches existing combo config editor patterns.

* feat(combo): wire ContextRequirementsEditor into combo config form

Adds context requirements section to combo edit page (strategy section),
matching existing ResponseValidation pattern. Placed after response
validation block, before agent features.

* docs(combo): add context requirements feature documentation

Covers: config schema, behavior, use cases, UI integration,
troubleshooting, and test instructions.

* fix(combo): pass provider+modelStr to getModelContextLimit for accurate context resolution

Per gemini-code-assist review feedback: model names are not globally
unique across providers. Passing both provider and modelStr ensures
correct context limit resolution in applyContextRequirements().

* fix(combo): repair broken doc links and restore test:unit:fast flag

- Point docs/combo-context-requirements.md 'Related' links at real docs
  (routing/AUTO-COMBO.md, architecture/RESILIENCE_GUIDE.md) — the three
  placeholder links (strategies.md/model-capabilities.md/fusion-tuning.md)
  did not exist and failed check:doc-links (Docs Gates fast-path).
- Revert an out-of-scope package.json change to test:unit:fast that dropped
  --test-isolation=none; restore to match release/v3.8.47.

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>

* test(combo): validate context requirements against the real Zod schema

Point tests/unit/combo-context-requirements.test.ts at the real
comboRuntimeConfigSchema export (src/shared/validation/schemas/combo.ts)
instead of hand-duplicating the Zod schema inline, so the test catches
schema drift.

Also declare contextRequirements on DEFAULT_COMBO_CONFIG so
resolveComboSetupConfig's inferred return type includes the key —
combo.ts reads config.contextRequirements but the property was missing
from the object typecheck:core infers types from, causing a build error.

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>

* fix(combos): remove dead ContextRequirementsEditor scaffolding (broken ui/card+label imports)

The editor imported @/components/ui/card and @/components/ui/label which do not
exist in the repo, breaking the Turbopack build. Removed the editor + its page.tsx
usage + doc mention; the real fix (comboConfig contextRequirements default + test)
is preserved.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
2026-07-12 03:41:50 -03:00
Ronaldo Davi
ecf226ece7 fix(release): read changelog.d fragments in list-uncovered-commits (#6857) (#6878)
* fix(release): count changelog.d fragment refs in list-uncovered-commits (#6857)

Since fragments-first (#6783), a merged PR's changelog entry usually lives in
changelog.d/{features,fixes,maintenance}/<PR>-<slug>.md and is only folded into
CHANGELOG.md at release time. list-uncovered-commits.mjs scanned only CHANGELOG.md,
so every fragment-covered commit was reported as an uncovered gap (some fragments —
e.g. 6708, 6709 — carry no #N in the body, only in the filename).

Add fragment-aware ref collection: fragmentFilenameRef() reads the leading <N>- of a
fragment filename, fragmentRefs() unions filename PR numbers with every #N in the body,
and collectChangelogRefs() unions the CHANGELOG scan window with the fragment refs.
main() now reads changelog.d via readChangelogFragments() and feeds it into the union.

On release/v3.8.47 tip this moves 44 commits from uncovered to covered (215/341 vs the
prior 171/341) without changing the covered/uncovered classification logic.

* chore(release): add changelog.d fragment for #6878

Housekeeping item requested in review: the PR fixing changelog-fragment
coverage tracking (#6857) did not itself have a changelog.d fragment.

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
2026-07-12 03:30:51 -03:00
backryun
67a0b99240 feat(providers): add GPT-5.6 model family (#6862)
* feat(providers): add GPT-5.6 model family

* fix(chatgpt-web): resume temporary chat handoffs

* fix(codex): auto-merge discovery, filter denylist, revalidate on lifecycle

Restore live/GitHub auto-merge for Codex catalogs, drop models via
explicit denylist (GPT-5.4 family), and run scrub+live re-sync once on
first-start, app upgrade, or setup completion. Success log:
kill deprecated models complete.

* fix(codex): preserve live catalog reconciliation

Expose remote-only Codex models without dropping user custom entries, and complete lifecycle revalidation only after a successful internal sync. Keep credentialed self-fetches pinned to the active dashboard listener.

---------

Co-authored-by: backryun <backryun@daonlab.local>
2026-07-12 03:30:47 -03:00
Dayna Blackwell
e7c0d93141 feat(compression): update vendored GCF (Headroom) codec to spec v3.2 — nested flattening (#6838)
* feat(compression): update vendored GCF (Headroom) codec to spec v3.2 (nested flattening)

Homogeneous arrays whose rows carry nested objects/arrays now tabularize
via GCF v3.2 `>`-path flattening instead of a low-yield per-row fallback,
so nested MCP tool-result rows (meta:{...}, tags:[...]) compact like flat
rows. Round-trip stays lossless (order-insensitive deepEqual).

Re-vendored from current gcf-typescript into the Headroom generic-profile
codec (open-sse/services/compression/engines/headroom/gcf/); still zero
runtime deps, MIT, SPDX-marked, generic-profile only. Also folds in two
upstream round-trip-safety fixes: the [N]: inline-array quoting fix and
canonical decimal formatting.

Regression guard: tests/unit/compression/headroom-smartcrusher.test.ts
gains a deep-nested case (two-level object + array-of-objects) asserting
the v3.2 flatten paths and order-insensitive round-trip. Vendored-code
baseline bumps (complexity 2053->2055, cognitive 885->888, decode_generic
no-explicit-any 18->22) each carry an inline _rebaseline_2026_07_10_gcf_v3_2
justification noting the growth is the vendored surface, not new project code.

* chore(changelog): add fragment for headroom GCF v3.2 nested flattening (#6838)

* docs(readme): note Headroom handles nested arrays (GCF v3.2) in the engine-stack table

* docs(readme): note Headroom handles nested arrays (GCF v3.2) in the engine-stack table

* fix(compression): harden vendored GCF decoder against prototype pollution

The v3.2 flatten/unflatten paths (and the pre-existing inline-object parser)
built decoded objects with bracket assignment and `key in obj` membership,
so a hostile or unusual payload could pollute Object.prototype via a
`__proto__` path segment, and any key shadowing an Object.prototype member
(`toString`, `constructor`) was misparsed or wrongly flagged duplicate.

- Encoder (`analyzeFlattenable`): builds the shape map with `Object.create(null)`
  and refuses to flatten objects carrying `__proto__`/`constructor`/`prototype`
  keys (they round-trip whole instead).
- Decoder: `unflattenPaths` drops any path with an unsafe segment; a shared
  `safeAssign` writes a literal `__proto__` key as an own data property
  (JSON.parse semantics) instead of reassigning the prototype, used at every
  object-build site; `checkDup` and orphan-merge use `hasOwnProperty` so
  built-in-named keys are not spuriously treated as duplicates.

Also a losslessness fix: objects with keys named `toString`/`constructor`/
`valueOf` now round-trip. Regression guard: prototype-pollution + built-in-key
cases in tests/unit/compression/headroom-smartcrusher.test.ts. Prototype
pollution is JS/TS-specific; the Go/Python/Rust/Swift/Kotlin SDKs use native
maps and are unaffected.

* fix(compression): apply GCF decoder review hardening (hasOwnProperty sweep, unflatten null-guard, strict count)

Addresses the second-round review on the vendored codec:
- Replace every `key in obj` membership test with
  `Object.prototype.hasOwnProperty.call(...)` across generic.ts (flatten
  shape analysis, key-chain resolution, inline-schema/shared-array helpers,
  row encode) so inherited names (`toString`/`constructor`) never match the
  prototype chain, and remove a redundant `obj` re-declaration in the ">"
  field attachment loop.
- `unflattenPaths` guards each intermediate segment: a missing OR non-object
  slot is replaced with a fresh object before traversal, so malformed/hostile
  input can no longer dereference a primitive and crash.
- Use the strict `parseCount` helper (not `parseInt`) for the shared-schema
  count so malformed counts fail the mismatch check instead of coercing.

The decoder grew past the 800-line file-size cap; frozen at 880 in
file-size-baseline.json with a justification (vendored file kept faithful to
upstream gcf-typescript for clean re-vendoring). Verified: prototype-pollution
+ hostile-input + built-in-key round-trip probes, 54/54 compression tests,
typecheck, lint, cyclomatic/cognitive baselines unchanged, compression-budget.

* fix(compression): do not flatten a nested object that is null in any row (losslessness)

analyzeFlattenable skipped null values during shape analysis, so a field that
was an object in some rows and null in others was still flattened. On decode,
the null row's leaves resolved as absent ("~") and unflattened to a missing key
instead of null, silently dropping the value (e.g. {meta:{owner:null}} decoded
to {}). analyzeFlattenable now bails (returns null) when the field is null in
any row, routing it through the lossless whole-object attachment path. Applies
at every nesting depth via the existing recursion. Regression guard: null
nested-object cases in tests/unit/compression/headroom-smartcrusher.test.ts.

* fix(compression): narrow the null-nested flatten bail to intermediate nulls only

The previous fix bailed flattening whenever a nested field was null in any row.
That is correct but over-broad: a top-level null round-trips losslessly through
flattening (it emits "-" and reconstructs via the all-null rule). Only a null at
an intermediate nesting level loses data (its leaves encode as absent "~" and
unflatten to a missing key). Bail only when parentPath is non-empty, so top-level
nulls keep flattening (compression preserved) while intermediate nulls fall back
to the lossless attachment path. Matches GCF conformance fixtures 004/013.

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
2026-07-12 03:30:43 -03:00
Diego Rodrigues de Sa e Souza
2d321e1f52 chore(security): scrub hardcoded live-instance creds from boundary tests (#6786)
The 3 tests/boundary/*.live.test.ts files merged via #6786 hardcoded a real
Bearer API key, an auth_token JWT cookie, and a live instance URL. Replaced with
env reads (OMNIROUTE_TEST_BASE/BEARER/COOKIE), preserving the RUN_BOUNDARY_LIVE gate.
The leaked key/cookie must still be revoked/rotated on the affected instance and
purged from history separately (operator action).
2026-07-12 02:53:37 -03:00
Paijo
b5e75bb8fe fix(proxy): relay repair + free-pool UX + relay awareness (#6909)
* feat(proxy): relay repair + free-pool UX + relay awareness

* fix(proxy): preserve existing notes fields on relay repair; fix cpu limit /1000 across all 5 container providers

* feat(proxy): extract bulk-import and pool-modal hooks (#6625)

* fix: prevent relay type normalization to http on PATCH

Bug: updateProxyRegistrySchema inherited .default("http") from the base
schema, causing PATCH to silently overwrite relay types (vercel/deno/cloudflare)
with "http" when the client didn't send a type field.

- Move .default('http') from proxyRegistryFieldsSchema to
  createProxyRegistrySchema (only applies to new proxies)
- Strip undefined keys from validated changes before passing to
  updateProxy — .partial() leaves absent fields as undefined, which
  the spread merge in updateProxyRow would propagate to the DB
- Add console.warn in extractRelayAuth when decrypt fails on a
  known-encrypted relayAuthEnc blob

Closes #6905

* feat: replace Load More with page-number pagination in FreePoolTab

- Adds page-number pagination controls with prev/next buttons
- Shows per-page summary with total counts
- Resets to page 1 on filter change via wrapper setters

* fix(proxy): restore free-proxy sync-error tracking reverted by pagination commit

commit 2c8e79f13 (page-number pagination for FreePoolTab) accidentally
reverted the recordFreeProxySyncErrors/clearFreeProxySyncErrors/
getFreeProxySyncErrors functions and the search/sortBy list options that
an earlier commit (6f9ce75f3) in this same PR had added to
src/lib/db/freeProxies.ts and src/app/api/settings/free-proxies/route.ts.

localDb.ts still re-exported the three sync-error functions, so every
module that transitively imports it (which is most of the unit test
suite, plus the Next.js build used by dast-smoke) failed at load time
with "The requested module './db/freeProxies' does not provide an
export named 'clearFreeProxySyncErrors'".

Restores the reverted implementation (verbatim) and fixes the two new
"requires management auth" tests, which asserted 401 for an
unauthenticated request without configuring INITIAL_PASSWORD +
requireLogin first — on a fresh DB with no password configured,
isAuthRequired() treats a loopback request as the pre-setup bootstrap
path and allows it through, so the assertion needs the same
password/requireLogin setup already used by
tests/unit/api/settings-audit.test.ts.

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

* fix(proxy): trim unrelated scope from relay-repair/free-pool PR

Remove two subsystems that are unrelated to relay repair and the
free-pool UX and were never wired into the app:

- open-sse/services/combo/capabilityRequirements.ts and
  CapabilityRequirementsEditor.tsx: a combo capability-filtering
  feature never imported by combo.ts or combos/page.tsx, with no
  test coverage.
- src/lib/skills/containerProvider.ts CPU-limit rescale: an
  untested change to sandbox resource limits, unrelated to the
  proxy/relay subsystem this PR targets. Restored to the
  release baseline.

Also renumber the free_proxy_sync_errors migration 121 -> 122 to
avoid colliding with #6855, which independently claims 121 on the
same release branch.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-12 02:28:26 -03:00
Jade Guo
10807972db fix(sse): default reasoning summary for effort-only Responses requests (#6807)
A Chat-Completions client can only express reasoning via the top-level
reasoning_effort hint and has no way to request a reasoning summary. When
that hint is promoted to the Responses API's reasoning.effort, the
upstream returns an empty summary and downstream chat clients see no
thinking stream (encrypted reasoning only).

Default reasoning.summary "auto" plus include ["reasoning.encrypted_content"]
on the effort-only path so the summary actually streams back to the chat
client, mirroring the Codex executor's ensureCodexReasoningSummary. An
explicit reasoning object from a Responses-shaped client is preserved
untouched, and reasoning_effort "none" is left without a summary.

Adds regression tests for the effort-only default, the none case, and
keeps the existing explicit-reasoning-object behavior unchanged.

Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
2026-07-12 02:21:38 -03:00
Diego Rodrigues de Sa e Souza
78ce492576 chore(quality): freeze file-size for #6909 (localDb 805) + #6807 (translator test 1195)
Owner-approved /merge-prs tail freeze. localDb.ts is re-export-only (Hard Rule #2);
translator test grew from #6807's regression suite. Both frozen (shrink-only).
2026-07-12 02:21:16 -03:00
Chirag Singhal
5cebefe64a fix(sse): treat compression no-op as zero-savings, not inflation/silent-drop (#6883)
A structural engine (ccr / session-dedup) that finds nothing to compress
returns the body unchanged. That no-op was mishandled three ways — the
code-level root cause of the #6465–#6493 "0% savings, no reason" symptom class:

A. Inflation guard mislabelled a no-op as inflation. guardPipelineInflation
   used `compressedTokens >= originalTokens`, so an unchanged body
   (compressedTokens === originalTokens) tripped the guard, setting
   fallbackApplied=true and emitting a misleading "did not shrink; reverted
   to original" warning. Changed to strict `>` — only a strictly larger
   output is inflation; equality is a no-op. Genuine inflation still reverts.

B. Disabled-engine skip was silent and asymmetric with the breaker skip. Both
   stacked loops (sync + async) skipped a registry-disabled engine with a bare
   `continue`, recording no validationWarning — while the sibling breaker-open
   branch does. Both loops now add
   `${engine}: skipped (engine disabled in registry)`, mirroring the breaker branch.

C. No-op engine lost its identity in engineBreakdown. mergeStackStep
   early-returned on null stats, pushing no breakdown entry, so
   ensureEngineBreakdown synthesized a generic "stacked" 0% node. It now records
   a zero-savings entry keyed on the engine that actually ran, preserving identity.

Tests: tests/unit/compression-noop-guard.test.ts covers A (equal-token no-op not
inflated; strictly-larger still reverts), B (disabled skip surfaces a "disabled"
warning), and C (no-op engine keeps its own id in the breakdown). Updated the
existing inflation-guard test whose net-zero case encoded the old buggy behaviour,
and switched its wire test to object-form pipeline steps so the intended engine runs.

Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
2026-07-12 02:04:25 -03:00
KooshaPari
37ea7aab2c fix(usage): strict validation for xAI exact provider-reported cost (#6856)
extractUsageFromResponse() and normalizeUsage() used Number(x)
coercion for cost_in_usd_ticks, which silently turned null/""
into 0 -- accepted downstream as a valid $0 exact cost instead of
falling back to the token-based estimate. Both call sites now
require typeof === "number" && Number.isFinite && >= 0.

Rebased onto current release/v3.8.47 tip (already carries #6711)
and trimmed to just the incremental validation fix + 2 regression
tests, replacing the stale-base diff that re-added the whole
already-merged feature.

Co-authored-by: KooshaPari <koosha@phenotype.io>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-12 02:04:20 -03:00
Diego Rodrigues de Sa e Souza
3164eafbd1 fix(providers): scope nvidia NIM 404s to the single failing model (#6773) (#6888)
* fix(providers): scope nvidia NIM 404s to the single failing model (#6773)

The nvidia registry entry multiplexes 17 models from 9 different upstream
vendors (z-ai/, minimaxai/, deepseek-ai/, qwen/, mistralai/, stepfun-ai/,
moonshotai/, openai/, nvidia/) behind one connection, but was missing
passthroughModels: true — unlike 34 other multi-model registries
(modelscope, synthetic, kilo-gateway, etc). Without it, hasPerModelQuota
returns false for nvidia, so a 404 on a single stale/renamed model falls
through checkFallbackError's generic catch-all as a connection-wide
cooldown instead of being scoped to just that model, poisoning all 17
nvidia models for the cooldown window.

Add passthroughModels: true to the nvidia registry entry so 404/429s on
one model lock out only that model.

Regression test: tests/unit/nvidia-passthrough-models-6773.test.ts

* fix(quality): register nvidia passthrough test in stryker tap.testFiles

check-mutation-test-coverage.mjs --strict flagged
tests/unit/nvidia-passthrough-models-6773.test.ts as an unregistered
covering test for accountFallback.ts (Fast Quality Gates).

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>
2026-07-12 02:00:32 -03:00
Diego Rodrigues de Sa e Souza
e3e474d0a6 fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) (#6887) 2026-07-12 02:00:23 -03:00
Diego Rodrigues de Sa e Souza
df3a6cf674 fix(providers): route AgentRouter key validation through CC wire image (#6377) (#6882)
* fix(providers): route AgentRouter key validation through CC wire image (#6377)

* test(6377): type fetch mock to satisfy no-explicit-any gate
2026-07-12 02:00:19 -03:00
Markus Hartung
49e0b7d667 fix(responses): escape literal control chars in tool call JSON; emit … (#6786)
* fix(responses): escape literal control chars in tool call JSON; emit status=failed on upstream error #6785

Two bugfixes in the Responses API translator:

1. escapeJsonStringValues() sanitizes tool call arguments containing
   literal 0x0A/0x0D/0x09 bytes (emitted by Gemma4 models) into valid
   JSON \n/\r/\t escapes, preventing SSE framing corruption. Only
   escapes inside JSON string contexts — already-escaped sequences
   and structural JSON pass through unchanged.

2. sendCompleted() checks state.upstreamError and emits status="failed"
   with error.code + error.message instead of silently hardcoding
   status="completed" + error=null, so mid-stream errors (e.g. Gemini
   503 after partial content) are properly surfaced to the client.

3. stream.ts: calls translateResponse(null,...) before controller.error()
   so the translator can emit close events (reasoning item done,
   response.completed) before the stream is terminated.

* test(boundary): fix ESLint no-explicit-any warnings and quality gates

Green the PR against release/v3.8.47 quality gates without weakening tests:

- Replace @typescript-eslint/no-explicit-any in the new boundary/gemma4
  tests with proper interfaces (ResponseBody, ToolDef, ToolArgs, SseEvent
  item accessors) — fixes the "No new ESLint warnings" gate.
- Split tests/unit/translator-resp-openai-responses.test.ts (1079 LOC) by
  extracting the round-trip suite into a sibling file so both stay under
  the 800-line test cap — fixes check:file-size.
- Rename the 5 live boundary tests to *.live.test.ts, gate them behind
  RUN_BOUNDARY_LIVE=1, add a test:boundary:live npm script and register the
  glob in check-test-discovery COLLECTORS — fixes check:test-discovery
  (they hit a live remote and must never run unopted in CI).

Co-authored-by: Markus Hartung <mail@hartmark.se>

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
2026-07-12 02:00:14 -03:00
Diego Rodrigues de Sa e Souza
a2ac387abd fix(cli): ship head-response-guard.cjs in the standalone bundle (#6908)
* fix(cli): ship head-response-guard.cjs in the standalone bundle

server-ws.mjs imports ./head-response-guard.cjs, but assembleStandalone had
no EXTRA_MODULE_ENTRIES entry for it, so every build:release bundle crashed
at boot with ERR_MODULE_NOT_FOUND (found deploying d1d75fdbf to the VPS on
2026-07-11). Adds the missing entry plus a regression test that derives the
required sidecar list from server-ws.mjs's own relative imports, guarding
the whole class of missing-sidecar bugs.

* docs(changelog): fragment for #6908
2026-07-12 02:00:09 -03:00
Diego Rodrigues de Sa e Souza
858598a200 fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401) (#6898)
archiveLegacyRequestLogs() swept the entire DATA_DIR/logs directory as a
single "legacy" target and recursively deleted it after zipping. Since
PR #6234 moved the default app-log path to DATA_DIR/logs/application,
the migration was deleting the live file logger's own directory on
every boot until its marker file existed (#6799).

Separately, yazl's addFile() does an internal stat-then-stream read; if
a target file grows between those two steps (e.g. an actively-written
log), yazl emits "error" directly on the ZipFile instance. That event
had no listener, so Node re-threw it as an uncaughtException that
crashed the whole process at startup (#6401) — misdiagnosed upstream as
Turbopack/Windows chunk corruption because the stack trace pointed into
a bundled chunk.

Fix:
- listArchiveTargets() now enumerates DATA_DIR/logs entries individually
  and skips the live app-logger directory (resolved via
  logEnv.getAppLogFilePath()), so the shared parent directory is never
  deleted wholesale.
- createLegacyArchive() wires a zipFile.on("error", ...) handler so a
  stat/stream race rejects the promise (caught by the existing
  try/catch) instead of escaping as an uncaughtException.

Regression test: tests/unit/usage-migrations-legacy-archive-safety.test.ts
(RED on unfixed code, GREEN after fix). Updated
tests/unit/request-log-migration.test.ts to the corrected contract —
DATA_DIR/logs itself now survives the archive sweep.

Gates run clean: file-size, complexity, cognitive-complexity,
changelog-integrity, typecheck:core, eslint (suppressions), and the
existing usage-migrations/request-log-migration unit suites.
2026-07-12 02:00:02 -03:00
Diego Rodrigues de Sa e Souza
0f3c68fa69 fix(sse): de-flake timing-sensitive combo cooldown/breaker tests (#6803) (#6897)
* fix(sse): de-flake timing-sensitive combo cooldown/breaker tests (#6803)

Extracts 3 wall-clock-sensitive assertions (combo-quota-share cooldown
ceiling x2, circuit-breaker HALF_OPEN race) into tests/unit/serial/
(--test-concurrency=1, the repo's established remedy for this class of
test) and widens their margins, since a starved CI-runner event loop can
blow even a serialized test's timing window. Also adds an explicit 30s
vitest timeout to the MCP audit shutdown test, which had no override and
inherited vitest's 5000ms default.

Regression proof: reproduced RED locally under real devbox CPU
contention (2644ms/1796ms elapsed vs the old 1500ms ceiling, exactly the
reported failure mode); confirmed GREEN after the fix under the same
contention.

* fix(quality): register new serial timing tests + prune stale any-suppression count

- stryker.conf.json: add tests/unit/serial/combo-quota-share-cooldown-wait-timing.test.ts
  and tests/unit/serial/combo-strategy-fallbacks-half-open-timing.test.ts to
  tap.testFiles so their mutant kills count for accountFallback.ts and
  circuitBreaker.ts (PR #6897 added these files but didn't register them).
- eslint-suppressions.json: combo-strategy-fallbacks.test.ts's no-explicit-any
  suppression count was stale (35) after this PR trimmed 2 any-usages out of
  the file when extracting the half-open timing test; corrected to 33.

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>
2026-07-12 01:59:57 -03:00
Diego Rodrigues de Sa e Souza
a6dfd1068e fix(providers): wire devin cloud-agent into provider validation and static models (#6142) (#6894) 2026-07-12 01:59:53 -03:00
Diego Rodrigues de Sa e Souza
c0682c1ac2 fix(cli): waitForServer must not report ready on bare TCP accept (#6800) (#6892)
waitForServer() polled /api/monitoring/health but fell back to declaring
the server ready once the port had merely accepted TCP connections for
>= 3s, even if no HTTP response was ever received. On CPU-bound warmup
(e.g. small VPS running Next.js standalone), the OS-level listener
accepts TCP almost immediately while the request pipeline is still
compiling, so the fallback fired within ~3-7s and the CLI printed
'OmniRoute is running!' 30-60s before any route actually answered.

Classify each health poll into ready / fast-reject / hanging /
not-listening: only a fast HTTP rejection (fetch error that is not a
timeout, e.g. ECONNRESET before the route mounts) grants the original
#2460 Windows-cold-start grace window. A request that times out with
zero response (the reported #6800 symptom) resets the grace window
instead of accumulating toward it.

Regression tests: tests/unit/waitForServer-tcp-fallback-6800.test.mjs
(new RED-then-GREEN probe from the bug analysis) and
tests/unit/cli-waitForServer.test.mjs (existing suite realigned to the
corrected contract, plus a new case for the hanging-socket scenario).
2026-07-12 01:59:49 -03:00
Diego Rodrigues de Sa e Souza
8c4364a597 fix(providers): give v0-vercel-web its own alias so credentials are detected (#6343) (#6891)
* fix(providers): give v0-vercel-web its own alias so credentials are detected (#6343)

* test(6343): type casts to satisfy no-explicit-any gate

* test(6343): register v0-web + cliproxyapi tests in stryker tap.testFiles

The two unit tests added on this branch cover mutated modules
(src/sse/services/auth.ts, comboContextCache.ts) but were missing from
stryker.conf.json tap.testFiles, tripping check:mutation-test-coverage --strict.

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-07-12 01:59:44 -03:00
Diego Rodrigues de Sa e Souza
0834bb2c70 fix(providers): strip redundant node prefix on connId-addressed custom models (#6772) (#6890) 2026-07-12 01:59:40 -03:00
Diego Rodrigues de Sa e Souza
305aa7646e fix(routing): honor no-auth provider connection isActive in auto-combo pool (#6557) (#6889) 2026-07-12 01:59:35 -03:00
Chirag Singhal
5097e3a111 fix(api): route error responses through sanitizeErrorMessage (Hard Rule #12) (#6886)
* fix(api): route error responses through sanitizeErrorMessage (Hard Rule #12)

9 API routes returned raw String(error)/error.message directly in HTTP 500
bodies, leaking SQLite paths, SQL text and internal messages. Route all through
sanitizeErrorMessage() per Hard Rule #12:

- settings/compression (GET+PUT), settings/compression/mcp-accessibility (GET+PUT)
- cache/entries (GET+POST), db/health (GET+POST), db-backups/exportAll
- assess, combos/test, settings/notion, settings/obsidian

Test: tests/unit/rule12-error-sanitization-sweep.test.ts asserts sanitized 500
bodies contain no absolute paths / stack tails.

* test(stryker): register rule12 error-sanitization sweep in tap.testFiles

The new tests/unit/rule12-error-sanitization-sweep.test.ts covers a mutated
module, so it must be listed in stryker.conf.json tap.testFiles for the
mutation-coverage gate (check-mutation-test-coverage.mjs --strict) to pass.

Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com>

---------

Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
2026-07-12 01:59:31 -03:00
Diego Rodrigues de Sa e Souza
ec1e79b8a7 fix(ci): exclude check-test-masking.test.ts fixtures from self-referential tautology gate (#6634) (#6884)
* fix(ci): exclude check-test-masking.test.ts fixtures from self-referential tautology gate (#6634)

* fix(ci): extend test-masking self-fixture exclusion to sibling gate regression files (#6634)

The #6634 fix added isSelfTestFixtureFile()/scanBareTautologies() exclusions
that only matched check-test-masking.test.ts exactly. Its own new regression
file check-test-masking-selfref-6634.test.ts also embeds tautology-pattern
literals as fixtures/documentation, so the absolute-floor scanBareTautologies
gate self-tripped a HARD failure on the PR's own file. Generalize the
exclusion to the whole check-test-masking* self-test family and lock it with
two regression tests.
2026-07-12 01:59:25 -03:00
Diego Rodrigues de Sa e Souza
993f280c4b fix(ci): publish electron-updater latest.yml manifests in release assets (#6766) (#6881)
* fix(ci): publish electron-updater latest.yml manifests in release assets (#6766)

* fix(ci): register new mutation-covering test + realign stale codex-cli version fixture

- stryker.conf.json: add tests/unit/cliproxyapi-model-mapping-dispatch.test.ts to
  tap.testFiles so it counts toward mutation coverage for the newly-added
  comboContextCache.ts coverage (check:mutation-test-coverage --strict was failing).
- tests/unit/provider-models-route-codex.test.ts: DEFAULT_CODEX_CLIENT_VERSION was
  bumped to 0.144.0 on release/v3.8.47 after this test's fixtures were written;
  realign the hardcoded 0.142.0 expectations to the current constant.
2026-07-12 01:59:21 -03:00
Chirag Singhal
47bc3317c1 fix(oauth): embed Trae OAuth client_id via resolvePublicCred (Hard Rule #11) (#6870)
* fix(oauth): embed Trae OAuth client_id via resolvePublicCred (Hard Rule #11)

* fix(quality): register tests/unit/trae-publiccred.test.ts in stryker tap.testFiles

The mutation test-coverage gate (check:mutation-test-coverage --strict) flags
new covering unit tests that mutate open-sse/utils/publicCreds.ts but aren't
listed in stryker.conf.json's tap.testFiles, so their mutant kills wouldn't
count. Register the new test file alphabetically.

Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com>

---------

Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
2026-07-12 01:59:15 -03:00
Chirag Singhal
d256fc367a fix(sse): stop combo path tripping whole-provider breaker on plain 429 (#6868)
* fix(sse): stop combo path tripping whole-provider breaker on plain 429

The combo path recorded a whole-provider circuit-breaker failure for a
plain rate-limit 429, opening the breaker after N consecutive 429s and
blocking every account+model on that provider. This contradicts the
single-model path and the documented RESILIENCE_GUIDE policy.

- Single-model path uses PROVIDER_BREAKER_FAILURE_STATUSES =
  Set([408, 500, 502, 503, 504]) (src/sse/handlers/chat.ts:206) — 429 excluded.
- Combo path gated shouldRecordProviderBreakerFailure on isProviderFailureCode
  (accountFallback.ts), whose PROVIDER_FAILURE_ERROR_CODES INCLUDES 429 for
  connection-cooldown scope — so a plain 429 wrongly tripped the whole-provider
  breaker.

Fix scopes tightly: comboPredicates now tests a local
PROVIDER_BREAKER_FAILURE_STATUSES set mirroring the single-model constant
(429 excluded), instead of isProviderFailureCode. The shared
isProviderFailureCode / PROVIDER_FAILURE_ERROR_CODES are deliberately left
untouched — they drive connection-cooldown / model-lockout logic where 429
must still count. A genuine quota/token-limit terminal 429 is handled
elsewhere; only the whole-provider breaker-recording gate changes.

Adds tests/unit/combo-breaker-429.test.ts covering the 429 exclusion,
the 408/5xx inclusion, and the sameProviderNext / skipProviderBreaker
suppression paths.

* test(quality): register combo-breaker-429.test.ts in stryker tap.testFiles

Fast Quality Gates' mutation-coverage drift check flagged this PR's new
covering test for comboPredicates.ts as unregistered.

Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com>

---------

Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
2026-07-12 01:59:09 -03:00
growab
c4c8af3965 feat(proxy): shorthand proxy formats + protocol header mode for bulk import (#6867)
* fix(electron): bump electron 42→43 + build better-sqlite3 from source (ABI 148) (#6605)

fix(electron): bump electron 42→43 + rebuild better-sqlite3 from source against the Electron ABI (148).

Electron 43 raises NODE_MODULE_VERSION to 148; better-sqlite3@12.11.1 has no electron-v148 prebuild, so the packaged app died with 'Nenhum driver SQLite disponível'. prepare-electron-standalone now compiles better-sqlite3 from source against the electron headers into build/Release (where 'bindings' resolves it). Validated by Electron Package Smoke (green) + local (node_register_module_v148).

Supersedes #6378. (--admin: the only reds are SonarQube/SonarCloud failing on a coverage-report artifact digest-mismatch — a GitHub Actions infra flake, not this diff; Sonar is green on main and the diff touches only the electron build.)

* deps: bump the development group across 1 directory with 6 updates (#6588)

deps: bump the development group (6 updates). Rebased onto current main; all checks green after the electron-smoke fix (#6605).

* fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump (#6620)

fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump.

undici 8.6+ changed ProxyAgent to forward plain-HTTP via request-proxy instead of CONNECT, breaking OAuth refresh through a connection proxy (501). proxyDispatcher now passes proxyTunnel:true. Validated: Unit Tests 3/8 (the OAuth-proxy test) green, new regression test green (fails without the fix on undici 8.7), SonarQube green.

Supersedes #6380. (--admin: the only red is Electron Package Smoke failing on a next-build artifact 'digest-mismatch' — a GitHub Actions infra flake corrupting the asar ('file data stream has unexpected number of bytes'); the better-sqlite3 rebuild itself succeeded (gyp ok) and the electron path is unchanged from #6605 which passed the smoke. Not this diff.)

* feat(proxy): add shorthand formats + protocol header mode for bulk import

Supports 6 new shorthand formats alongside the existing pipe-delimited
parser:
  - ip:port
  - ip:port:user:pass
  - user:pass@ip:port
  - user:pass:ip:port
  - protocol://ip:port
  - protocol://user:pass@ip:port

Protocol header mode: a bare protocol name (http/https/socks5) on its
own line sets the default type for subsequent protocol-less shorthand
lines. Explicit protocol:// prefix always takes precedence over the
header default.

Changes:
- Rewrite parseBulkProxyImport.ts with parseShorthandLine helper
- Use Record<string, true> for static lookup tables (VALID_PROXY_TYPES,
  VALID_PROXY_STATUSES) per project convention
- Add looksLikeHost() heuristic to disambiguate 4-colon format
  (ip:port:user:pass vs user:pass:ip:port)
- Update BULK_IMPORT_TEMPLATE in ProxyRegistryManager.tsx with full
  documentation and examples for all formats
- Update en.json bulkImportDescription to list all supported formats
- Add 30 unit tests covering every format, edge cases, and regressions
  for the existing pipe-delimited path

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
2026-07-12 01:59:03 -03:00
AgentKiller45
1f9ac3628b fix(sse): combo model lockout honors parsed upstream quota reset (#6863) (#6866)
* fix(sse): combo model lockout honors parsed upstream quota reset (#6863)

The combo failure path recorded model lockouts from checkFallbackError's
cooldownMs only, discarding quotaResetHintMs — the ungated channel that
carries a parsed upstream quota reset (e.g. Antigravity 429 "Resets in
92h27m28s"). With OAuth profiles defaulting useUpstreamRetryHints=false,
the lockout fell back to the base cooldown (seconds), so quota-dead
accounts were re-walked serially by every combo request for days
(measured 122s per request, 494s worst case in #6863).

Thread max(cooldownMs, quotaResetHintMs) into selectLockoutCooldownMs at
both combo lockout sites, mirroring the single-model path pattern in
src/sse/services/auth.ts (v3.8.43). All three resilience fences are
preserved: useUpstreamRetryHints still gates connection cooldowns, the
hint only affects model-scope lockouts, and combo 429s remain
non-persistent.

TDD: tests/unit/combo-lockout-quota-reset-6863.test.ts fails on base
(lockout 5000ms) and passes with the fix (~92.5h).

* test(sse): tighten #6863 lockout assertion to parsed-reset bounds; prettier pass

Assert remainingMs falls within (parsedResetMs - 5s, parsedResetMs] so a
hardcoded long cooldown cannot satisfy the regression test. Also apply
Prettier to both changed files (includes one pre-existing formatting fix
in handleRoundRobinCombo picked up by --write).

* fix(sse): align combo lockout hint selection with single-model path; register test in mutation gate

Adopt review feedback: replace Math.max(cooldownMs, quotaResetHintMs) with
the auth.ts pattern (usedUpstreamRetryHint ? cooldownMs : quotaResetHintMs)
so a parsed reset SHORTER than the fallback cooldown wins too — e.g. the
subscription-quota branch returns a 1h fallback while the body says
"resets in 45m"; max() would over-lock by 15 minutes.

Add a regression test for the short-reset case (fails against the max()
variant) and register the new test file in stryker.conf.json tap.testFiles
to satisfy check:mutation-test-coverage --strict.

* chore(quality): register cliproxyapi dispatch test in mutation gate

tests/unit/cliproxyapi-model-mapping-dispatch.test.ts landed on
release/v3.8.47 via #6903 without a tap.testFiles entry, so
check:mutation-test-coverage --strict fails on the branch tip and on
every PR merge ref. Register it so the gate is green again.

---------

Co-authored-by: judy459 <JUDYZHU459@outlook.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
2026-07-12 01:58:58 -03:00
Imam Wahyu Widodo
6530c92aa6 feat(provider): add OpenVecta AI inference gateway (#6833)
* fix(electron): bump electron 42→43 + build better-sqlite3 from source (ABI 148) (#6605)

fix(electron): bump electron 42→43 + rebuild better-sqlite3 from source against the Electron ABI (148).

Electron 43 raises NODE_MODULE_VERSION to 148; better-sqlite3@12.11.1 has no electron-v148 prebuild, so the packaged app died with 'Nenhum driver SQLite disponível'. prepare-electron-standalone now compiles better-sqlite3 from source against the electron headers into build/Release (where 'bindings' resolves it). Validated by Electron Package Smoke (green) + local (node_register_module_v148).

Supersedes #6378. (--admin: the only reds are SonarQube/SonarCloud failing on a coverage-report artifact digest-mismatch — a GitHub Actions infra flake, not this diff; Sonar is green on main and the diff touches only the electron build.)

* deps: bump the development group across 1 directory with 6 updates (#6588)

deps: bump the development group (6 updates). Rebased onto current main; all checks green after the electron-smoke fix (#6605).

* fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump (#6620)

fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump.

undici 8.6+ changed ProxyAgent to forward plain-HTTP via request-proxy instead of CONNECT, breaking OAuth refresh through a connection proxy (501). proxyDispatcher now passes proxyTunnel:true. Validated: Unit Tests 3/8 (the OAuth-proxy test) green, new regression test green (fails without the fix on undici 8.7), SonarQube green.

Supersedes #6380. (--admin: the only red is Electron Package Smoke failing on a next-build artifact 'digest-mismatch' — a GitHub Actions infra flake corrupting the asar ('file data stream has unexpected number of bytes'); the better-sqlite3 rebuild itself succeeded (gyp ok) and the electron path is unchanged from #6605 which passed the smoke. Not this diff.)

* feat(provider): add OpenVecta AI inference gateway

OpenVecta (https://openvecta.com/) is an OpenAI-compatible AI inference
gateway hosting LLMs (GLM, Claude, DeepSeek, GPT OSS, Llama, Kimi,
Nemotron...) plus text-embedding-* models behind a single Bearer key.

Wiring (7 integration points):
- src/shared/constants/providers/apikey/inference-hosts.ts: catalog entry
- open-sse/config/providers/registry/openvecta/index.ts: registry w/ 9 seed LLMs
- open-sse/config/providers/index.ts: wire into REGISTRY
- src/app/api/providers/[id]/models/discovery/providerModelsConfig.ts: live /v1/models URL
- src/app/api/providers/[id]/models/discovery/providerSets.ts: NAMED_OPENAI_STYLE_PROVIDERS
- public/providers/openvecta.svg: brand icon
- tests/unit/openvecta-provider-registration.test.ts: regression guard (6 tests, all pass)

No executor needed — buildOpenAiCompatibleRegistryEntry wires
format=openai / executor=default / authType=apikey / authHeader=bearer.

Live catalog discovery uses the existing NAMED_OPENAI_STYLE_PROVIDERS
path (live /v1/models fetch + registry seed as offline fallback).

Validation:
- npm run typecheck:core         clean
- npm run typecheck:noimplicit:core 4 errors in unchanged files (combo.ts, cliRuntime.ts); 0 in new code
- npm run lint                    clean
- node --import tsx/esm --test tests/unit/openvecta-provider-registration.test.ts   6/6 pass
- sibling tests/unit/openai-style-providers-4239-4155-3841.test.ts  18/18 pass (no regression)

* chore(merge): drop unrelated main-drift from PR fork + fix count/golden drift

The fork branch predated main's electron 42→43 bump (#6605) and several other
package.json/lockfile churn; those files are unrelated to the OpenVecta
provider addition and were reintroducing an older/stale state (version
3.8.46, electron 42, older bun/eslint-config-next) that broke the Electron
Package Smoke check. Restored package.json, package-lock.json,
electron/package.json, electron/package-lock.json, and
scripts/build/prepare-electron-standalone.mjs to match
origin/release/v3.8.47.

Also updates the two provider-count assertions (166->167) and regenerates
the translate-path golden snapshot to account for the new openvecta entry.

Co-authored-by: hajilok <120608486+hajilok@users.noreply.github.com>

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
2026-07-12 01:58:53 -03:00
dependabot[bot]
84c437d19e chore(deps): bump github/codeql-action/init from 4.36.3 to 4.37.0 (#6832)
* fix(electron): bump electron 42→43 + build better-sqlite3 from source (ABI 148) (#6605)

fix(electron): bump electron 42→43 + rebuild better-sqlite3 from source against the Electron ABI (148).

Electron 43 raises NODE_MODULE_VERSION to 148; better-sqlite3@12.11.1 has no electron-v148 prebuild, so the packaged app died with 'Nenhum driver SQLite disponível'. prepare-electron-standalone now compiles better-sqlite3 from source against the electron headers into build/Release (where 'bindings' resolves it). Validated by Electron Package Smoke (green) + local (node_register_module_v148).

Supersedes #6378. (--admin: the only reds are SonarQube/SonarCloud failing on a coverage-report artifact digest-mismatch — a GitHub Actions infra flake, not this diff; Sonar is green on main and the diff touches only the electron build.)

* deps: bump the development group across 1 directory with 6 updates (#6588)

deps: bump the development group (6 updates). Rebased onto current main; all checks green after the electron-smoke fix (#6605).

* fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump (#6620)

fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump.

undici 8.6+ changed ProxyAgent to forward plain-HTTP via request-proxy instead of CONNECT, breaking OAuth refresh through a connection proxy (501). proxyDispatcher now passes proxyTunnel:true. Validated: Unit Tests 3/8 (the OAuth-proxy test) green, new regression test green (fails without the fix on undici 8.7), SonarQube green.

Supersedes #6380. (--admin: the only red is Electron Package Smoke failing on a next-build artifact 'digest-mismatch' — a GitHub Actions infra flake corrupting the asar ('file data stream has unexpected number of bytes'); the better-sqlite3 rebuild itself succeeded (gyp ok) and the electron path is unchanged from #6605 which passed the smoke. Not this diff.)

* chore(deps): bump github/codeql-action/init from 4.36.3 to 4.37.0

Bumps [github/codeql-action/init](https://github.com/github/codeql-action) from 4.36.3 to 4.37.0.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](54f647b7e1...99df26d4f1)

---
updated-dependencies:
- dependency-name: github/codeql-action/init
  dependency-version: 4.37.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-12 01:58:48 -03:00
dependabot[bot]
067d0d6ed7 chore(deps): bump github/codeql-action/analyze from 4.36.3 to 4.37.0 (#6831)
* fix(electron): bump electron 42→43 + build better-sqlite3 from source (ABI 148) (#6605)

fix(electron): bump electron 42→43 + rebuild better-sqlite3 from source against the Electron ABI (148).

Electron 43 raises NODE_MODULE_VERSION to 148; better-sqlite3@12.11.1 has no electron-v148 prebuild, so the packaged app died with 'Nenhum driver SQLite disponível'. prepare-electron-standalone now compiles better-sqlite3 from source against the electron headers into build/Release (where 'bindings' resolves it). Validated by Electron Package Smoke (green) + local (node_register_module_v148).

Supersedes #6378. (--admin: the only reds are SonarQube/SonarCloud failing on a coverage-report artifact digest-mismatch — a GitHub Actions infra flake, not this diff; Sonar is green on main and the diff touches only the electron build.)

* deps: bump the development group across 1 directory with 6 updates (#6588)

deps: bump the development group (6 updates). Rebased onto current main; all checks green after the electron-smoke fix (#6605).

* fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump (#6620)

fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump.

undici 8.6+ changed ProxyAgent to forward plain-HTTP via request-proxy instead of CONNECT, breaking OAuth refresh through a connection proxy (501). proxyDispatcher now passes proxyTunnel:true. Validated: Unit Tests 3/8 (the OAuth-proxy test) green, new regression test green (fails without the fix on undici 8.7), SonarQube green.

Supersedes #6380. (--admin: the only red is Electron Package Smoke failing on a next-build artifact 'digest-mismatch' — a GitHub Actions infra flake corrupting the asar ('file data stream has unexpected number of bytes'); the better-sqlite3 rebuild itself succeeded (gyp ok) and the electron path is unchanged from #6605 which passed the smoke. Not this diff.)

* chore(deps): bump github/codeql-action/analyze from 4.36.3 to 4.37.0

Bumps [github/codeql-action/analyze](https://github.com/github/codeql-action) from 4.36.3 to 4.37.0.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](54f647b7e1...99df26d4f1)

---
updated-dependencies:
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.37.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-12 01:58:43 -03:00
Diego Rodrigues de Sa e Souza
4effdfddca chore(quality): rebaseline cognitiveComplexity 885->890 (v3.8.47 merge-train burst)
Owner-approved merge-burst reconciliation. cognitive-complexity does not run on
PR->release fast-gates, so incidental growth across the 23-PR merge-ready batch
accrued unmeasured (measured 890 on the combined merge-train tip vs 885 pristine).
2026-07-12 00:24:37 -03:00
Diego Rodrigues de Sa e Souza
1b7a9150e5 chore(ci): fix shared base-reds blocking PR queue (stryker registration + codex 0.144 test)
- register tests/unit/cliproxyapi-model-mapping-dispatch.test.ts in stryker.conf.json tap.testFiles (gap from #6903)
- update provider-models-route-codex.test.ts client_version 0.142.0 -> 0.144.0 (stale test from #6780 prod bump)
2026-07-11 22:42:52 -03:00
Chirag Singhal
6973e2bd34 fix(api): return 400 (not 500) on malformed JSON body (#6871)
Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com>
2026-07-11 21:36:33 -03:00
Chirag Singhal
a7227f4ef3 fix(fusion): select judge from a surviving panel member when no explicit judge (#6869)
When no explicit judgeModel is configured, the judge defaulted to panel[0]
before fan-out and was never reassigned. If panel[0] failed fan-out (timeout /
rate-limit / dropped straggler → it lands in `failures`, not `answers`), the
multi-answer synthesis path still dispatched the judge to that dead panel[0],
erroring the whole fusion request even though a quorum of other panel members
succeeded — exactly the failure fusion exists to tolerate.

Resolve the effective synthesis judge from a survivor when no explicit judge is
set: prefer panel[0] only when it survived, otherwise the first surviving
answer. An explicitly configured judge is still honored unchanged (operator
intent), and the answers.length===0 (503) and single-survivor branches keep
their existing semantics.

Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com>
2026-07-11 21:36:29 -03:00
MikeTuev
14c182ff37 fix(dashboard): logs detail modal no longer reopens on first close (#6830)
LogsPage recomputed initialId from window.location on every render, but the
App Router syncs window.location only after the navigation commits. Closing
the detail modal re-rendered the page while the URL still carried ?id=X, so
initialSelectedId flipped null -> X and the child's one-shot deep-link effect
(guard still unarmed after the open-click render, where location was stale in
the other direction) reopened the modal. Only the second close worked.

Read the id once via lazy useState so the prop stays stable for the page's
lifetime; deep links still open the modal on mount.

Regression test reproduces the App Router ordering with a router.replace mock
that re-renders the page before committing the URL.
2026-07-11 21:36:25 -03:00
Diego Rodrigues de Sa e Souza
0a358c02ab fix(api): merge id-only tool_call continuation deltas in stream summary (#6276) (#6905) 2026-07-11 11:22:22 -03:00
Diego Rodrigues de Sa e Souza
adb1fc5b27 fix(providers): honor max_token capability override in reasoning buffer clamp (#6524) (#6904)
getExplicitModelOutputCap() (the clamp ceiling used by
resolveReasoningBufferedMaxTokens) only ever read the unvalidated synced
limit_output / registry / static-spec chain — it ignored the operator-settable
max_token capability override that getResolvedModelCapabilities() already
consulted. When a provider's synced catalog row reports a wrong limit_output
(e.g. ollama-cloud/deepseek-v4-flash: limit_output=1048576, same as
limit_context, while the real upstream cap is 65536), the reasoning-buffer
clamp trusted the bad number and inflated max_tokens 64000 -> 96000, which
upstream rejected with "exceeds model's maximum output tokens (65536)".

The override table (model_capability_overrides, "max_token" key,
/api/model-capability-overrides) is the existing, already-shipped remediation
path for exactly this class of bad catalog data, but reasoningTokenBuffer.ts
had no way to benefit from it. Extracted the override lookup into a shared
getMaxTokenCapabilityOverride() helper and made getExplicitModelOutputCap()
consult it first, so both read paths now agree.
2026-07-11 11:22:15 -03:00
Diego Rodrigues de Sa e Souza
6dff715ba6 fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) (#6903) 2026-07-11 11:22:08 -03:00
Diego Rodrigues de Sa e Souza
5c0a0d8db9 fix(mcp): de-duplicate TOTAL_MCP_TOOL_COUNT by tool name (#6854) (#6902)
TOTAL_MCP_TOOL_COUNT in open-sse/mcp-server/server.ts summed collection sizes
additively, double-counting tools registered in more than one collection.
The agent-skills trio (omniroute_agent_skills_list/get/coverage) is
intentionally defined in both MCP_TOOLS (schemas/tools.ts) and
agentSkillTools (tools/agentSkillTools.ts), inflating the reported count
from 96 unique tools to 99.

Replace the additive sum with countUniqueMcpTools() (new
open-sse/mcp-server/toolCount.ts), which unions all collection tool
names into a Set before counting, so any future overlap self-corrects
instead of double-counting.

Regression test: tests/unit/mcp-tool-count-dedup-6854.test.ts
2026-07-11 11:22:00 -03:00
Diego Rodrigues de Sa e Souza
c089ca9d1a fix(compression): surface silently-dropped stacked-pipeline steps and fix inflation-guard no-op misfire (#6479, #6480, #6491) (#6901)
Two related root causes in the stacked compression pipeline:

- #6479/#6491: a dispatched step whose engine legitimately finds nothing
  eligible (session-dedup with no repeated blocks, ccr below its min-chars
  threshold) returns `{ stats: null }`. `mergeStackStep()` silently dropped
  that step from `engineBreakdown` with zero trace — no warning, no error.
  Now records a `"<engine>: skipped (no eligible content)"` validation
  warning for any null-stats step, covering every engine that follows this
  convention (session-dedup, ccr, headroom, relevance, llm, llmlingua,
  ionizer, readLifecycle), not just the two reported.

- #6480: `finalizeStackedResult` ran the aggregate `guardPipelineInflation`
  check unconditionally, even when the loop-level `compressed` flag stayed
  false (no step ever advanced `currentBody`). Since tokens are trivially
  equal when nothing ran, the guard mislabeled a genuine no-op as
  `fallbackApplied: true` with a misleading "reverted to original" warning.
  Extracted the guard into `applyStackedInflationGuard()` in
  `pipelineGuards.ts` (keeps `strategySelector.ts` under its frozen line
  budget) and gated it on `compressed === true`.

Also fixes `compression-pipeline-inflation-guard.test.ts`'s wire test,
which passed a bare engine-id string to the pipeline; `normalizePipelineStep()`
only recognizes a fixed set of built-in string aliases and silently
downgrades any other string to `{ engine: "caveman" }`, so the test's
custom inflating engine was never actually exercised. Passing a step object
restores the test's original intent.

New regression tests: tests/unit/compression/repro-6479-6491-null-stats-silent-drop.test.ts,
tests/unit/compression/repro-6480-noop-guard-misfire.test.ts.
2026-07-11 11:21:52 -03:00
Diego Rodrigues de Sa e Souza
9c1db94c74 fix(plugin): split OC-gate provider id from OmniRoute-facing routing id (#6859) (#6900)
resolveOmniRoutePluginOptions() auto-prefixes providerId with "opencode-"
(commit 75b52e286) so OpenCode 1.17.8+'s native-adapter gate accepts it as a
registered provider id. That prefixed value was being reused for the
OmniRoute-server-facing identifiers too: mapRawModelToModelV2's id/providerID,
mapComboToModelV2's providerID, and the dynamic provider hook's combo catalog
keys. OmniRoute's server has no "opencode-<x>" provider alias, so every
dispatched model failed credential lookup with "No credentials for
opencode-omniroute" / "No active credentials for provider:
opencode-omniroute".

Add a second, unprefixed omnirouteProviderId field and thread it through the
four dynamic-hook call sites that emit server-facing identifiers, while
leaving the OC-gate-prefixed providerId in place for AuthHook.provider,
provider registration (hook.id), and the static-catalog path (which OC
strips before dispatch, per the existing static-block comments).
2026-07-11 11:21:45 -03:00
Diego Rodrigues de Sa e Souza
ec553dd9c0 fix(db): share sql.js preinit across callers, fix named-param bind (#6628, #6802) (#6899)
- preInitSqlJs() now memoizes an in-flight Promise (not just the resolved
  adapter) per filePath, so concurrent BATCH/STARTUP/HealthCheck/
  ProviderLimitsSync callers at boot share one full-file read+WASM decode
  instead of each independently reloading the whole database — the
  thundering-herd amplifier of the OOM condition #6632 already partly
  fixed, left un-implemented by the reporter's own proposed fix (#6628).

- sqljsAdapter's run/get/all now unwrap a lone named-parameter object
  (e.g. .all({ isActive: 1 }) for "WHERE is_active = @isActive", the same
  call shape getProviderConnections() already uses against better-sqlite3)
  before calling sql.js's stmt.bind(), expanding it to the @/:/$ sigil
  variants sql.js's own named-bind path requires. Previously the object
  was wrapped into an array and sql.js took the positional-bind path,
  throwing "Wrong API use : tried to bind a value of an unknown type
  ([object Object])." whenever the sql.js WASM fallback driver was active
  — exactly the error #6802 reported (misattributed to better-sqlite3).

Regression tests added to tests/unit/db-adapters/driverFactory.test.ts and
tests/unit/db-adapters/sqljsAdapter.test.ts, both proven RED against the
prior code and GREEN after the fix.
2026-07-11 11:21:37 -03:00
Diego Rodrigues de Sa e Souza
ac81235609 fix(dashboard): surface Claude extraUsage credits in quota card (#6806) (#6896)
Enterprise-tier Claude accounts (default_raven_enterprise) don't get
five_hour/seven_day utilization windows from Anthropic's OAuth usage
endpoint — only an extra_usage credit-billing block. parseClaude()
only read data.quotas, so quotas stayed {} and the dashboard showed
"No quota data" even when extraUsage showed the account 100%
exhausted. parseClaude() now folds an enabled extraUsage block into a
credits-style quota row (mirroring parseCodex's bankedResetCredits
pattern), both when quotas is empty and when it's already populated.
2026-07-11 11:21:30 -03:00
Diego Rodrigues de Sa e Souza
69e47cdec5 fix(providers): honor a provider-level proxy assigned to no-auth providers (#6272) (#6895)
No-auth providers (mimocode, opencode, ...) are always dispatched with a single
hardcoded connectionId ("noauth" — SYNTHETIC_NOAUTH_CONNECTION_ID in
src/sse/services/auth.ts). No provider_connections row ever has id="noauth", so
resolveProxyForConnection() in src/lib/db/settings.ts could never populate
connectionRecord for them, and its provider-level proxy lookup (Steps 6/8) only
runs when connectionRecord is present. A proxy assigned via Settings -> Providers
-> mimocode was therefore silently ignored, reproducing the reporter's "same
thing happen when i set the proxy directly in the provider menu" symptom.

Adds a best-effort fallback (src/lib/db/settings/noAuthProxyFallback.ts): when
connectionRecord could not be resolved, scan the known no-auth provider ids for a
configured provider-level proxy (registry first, then legacy) before falling
through to the global/direct steps.

Regression test: tests/unit/proxy-noauth-provider-6272.test.ts (RED on unfixed
code — resolved to level=direct/proxy=null; GREEN after the fix).
2026-07-11 11:21:22 -03:00
Diego Rodrigues de Sa e Souza
afbd9361a7 fix(routing): recognize Kimi token-limit 400 as context overflow for combo fallback (#6637) (#6893)
combo.ts's isContextOverflow400() guard required the literal word
'context' in the 400 error body before letting a combo fall through to
the next target. Kimi's exact wording ('Your request exceeded model
token limit: 262144 (requested: 308458)') never says 'context', so the
guard misclassified it as a body-specific error and halted the whole
combo instead of trying the next (larger-context) target.

accountFallback.ts's CONTEXT_OVERFLOW_PATTERNS already recognized this
wording one layer below (via checkFallbackError -> shouldFallback), so
the two independently-maintained classifiers disagreed and the
stricter one won. Export CONTEXT_OVERFLOW_PATTERNS from
accountFallback.ts and reuse it inside combo.ts's
isContextOverflow400() so both layers share a single source of truth.

Regression test: tests/unit/repro-6637-kimi-token-limit.test.ts
(RED on unfixed code -> GREEN after the fix). Existing #4519 guard
tests (tests/unit/combo-param-validation-fallback-4519.test.ts) still
pass, including the negative case that a genuinely body-specific 400
is NOT misclassified as overflow.
2026-07-11 11:21:14 -03:00
Diego Rodrigues de Sa e Souza
a49ac1755d fix(docs): document Turbopack build memory tradeoff for RAM-constrained machines (#6409) (#6885) 2026-07-11 11:20:56 -03:00
backryun
d1d75fdbf4 ci(quality): cut PR gate wall time without dropping protection (#6716)
Collapse duplicate CI spend while keeping each gate's existence reason:

- quality.yml: TIA __RUN_ALL__ defers full unit to fast-unit 4-shard (#6781);
  path filters via classify-pr-changes; docs-gates split; draft skip
- ci.yml: wire docs/i18n/code path filters; ESLint JSON artifact for quality-gate;
  drop advisory typecheck:noimplicit; float actions/cache@v6
- TIA parity: memory/usage/combo/serial; **/*.test.mjs any depth; electron/bin
  no longer force unit __RUN_ALL__
- check:complexity-ratchets: one ESLint walk, ruleId-isolated baselines + cache
- check:api-docs-refs + lib/apiRoutes: shared API route inventory
- husky pre-push: intentionally light (gates live in pre-commit); CLAUDE.md +
  QUALITY_GATES.md docs synced
- collect-metrics / lint:json: path.resolve cache path; Windows-safe eslint bin
- env-doc allowlist for ESLINT_RESULTS_JSON / COMPLEXITY_ESLINT_REPORT
- release-green --full-ci expects check:api-docs-refs (not docs-symbols alone)

Tests: select-impacted, classify-pr-changes, api-routes lib, complexity-rule-count,
validate-release-green.

Reconciled after #6781 (fast-unit 2→4 shards) per maintainer request on #6716.

Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
2026-07-11 08:03:11 -03:00
Diego Rodrigues de Sa e Souza
2263377530 refactor(usage): extract per-group parsing in antigravityWeeklyQuota (cognitive-complexity gate 886→885, release-level drift from #6818 merge) 2026-07-11 08:01:46 -03:00
Someres
878d80eaeb fix(codex): bump default client version to 0.144.0 (#6780)
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-11 07:39:55 -03:00
Diego Rodrigues de Sa e Souza
8f90618308 feat: add Z.ai Web free web-cookie provider (#4056) (#6823)
* feat(providers): add Z.ai Web free web-cookie provider (#4056)

New zai-web web-session provider drives the free chat.z.ai consumer
chat UI via a pasted browser cookie, distinct from the existing
API-key zai/glm/glm-cn/glmt providers (api.z.ai). ZaiWebExecutor posts
to chat.z.ai/api/chat/completions with the cookie forwarded both as
Cookie and Authorization: Bearer <token>, and normalizes both z.ai's
internal delta_content/phase SSE envelope and a pass-through
OpenAI-shaped choices[].delta frame into standard chat-completion
chunks.

Registered in WEB_COOKIE_PROVIDERS, WEB_SESSION_CREDENTIAL_REQUIREMENTS,
the provider registry (GLM-4.6/4.5/4.5V models), the executor factory,
and tokenExtractionConfig.ts for in-app cookie capture.

* fix(providers): regenerate translate-path golden for zai-web + reduce cognitive complexity

* fix(providers): rename ZaiWebExecutor.buildHeaders to avoid incompatible BaseExecutor override

* chore(merge): re-sync with release/v3.8.47; move changelog bullet to changelog.d fragment (merge-storm proof)
2026-07-11 07:39:39 -03:00
Diego Rodrigues de Sa e Souza
d838be8df8 feat(usage): surface Antigravity weekly quota alongside the 5-hour window (#4017) (#6818)
* feat(usage): surface Antigravity weekly quota alongside the 5-hour window (#4017)

Antigravity enforces both a 5-hour and a weekly usage limit, but the agy/antigravity
quota widget only exposed the 5-hour window. The weekly limit isn't in the per-model
retrieveUserQuota response already fetched — it lives in a separate, undocumented
retrieveUserQuotaSummary RPC that groups models into families (Gemini Models, Claude
and GPT models) with one weekly bucket per family.

Adds a self-contained usage/antigravityWeeklyQuota.ts leaf: a cached, best-effort
fetch of that RPC + a pure parser that extracts the weekly-labeled bucket per group
(window inferred from bucketId/displayName text, matching the reverse-engineered
shape documented by third-party Antigravity clients) into gemini_weekly/
claude_gpt_weekly quota entries, merged into the existing quotas map the widget
already renders generically. A failed/unavailable RPC never affects the existing
per-model quotas.

Live VPS validation attempt (192.168.0.15, real antigravity account): both
retrieveUserQuota and retrieveUserQuotaSummary currently return 429
RESOURCE_EXHAUSTED for that account, so the live response shape could not be
captured directly. The parser was instead validated via TDD against the bucket
shape documented by CodexBar (steipete/CodexBar), a third-party Antigravity client
that reverse-engineered the same RPC, and is defensive against both response
envelopes it has observed (top-level groups[] and nested quotaSummary.groups[]).

* chore(merge): re-sync with release/v3.8.47 (restore CHANGELOG, keep own bullet)

* chore(merge): re-sync with release/v3.8.47; move changelog bullet to changelog.d fragment (merge-storm proof)
2026-07-11 07:39:24 -03:00
Diego Rodrigues de Sa e Souza
1045e57aa1 feat(codex): echo requested effort-suffixed model id in Responses payloads (#3697) (#6820)
* feat(codex): echo requested effort-suffixed model id in Responses payloads (#3697)

Codex CLI compatibility shim: the Responses API response.created/
response.in_progress/response.completed payloads now carry a `model`
field (previously absent), and for Codex-CLI-originated requests it
echoes the client-requested effort-suffixed model id (e.g.
gpt-5.5-xhigh) instead of the bare upstream id (gpt-5.5), so the Codex
CLI status line/model button shows the active reasoning effort.

- openai-responses.ts translator threads the upstream model into the
  Responses event objects (additive, omitted when unknown).
- New isCodexOriginatedHeaders() (codexIdentity.ts) reuses PR #3481's
  originator/User-Agent detection, header-based so it still fires when
  a combo routes codex/gpt-5.5-xhigh to a non-codex upstream.
- chatCore's existing opt-in #1311 echoModel pipeline now also fires
  automatically for Codex clients on the Responses API, regardless of
  the echoRequestedModelName setting.
- responseModelEcho.ts now also rewrites the nested response.model
  field the Responses API uses (previously only top-level model).
- /v1/models keeps returning models: [] for Codex (unchanged, #3481).

Regression guard: tests/unit/codex-effort-model-echo-3697.test.ts.

Closes #3697

* chore(merge): re-sync with release/v3.8.47 (restore CHANGELOG, keep own bullet)

* chore(merge): re-sync with release/v3.8.47; move changelog bullet to changelog.d fragment (merge-storm proof)
2026-07-11 07:39:09 -03:00
Jan Leon
d242e225de Discover live Codex models (#6776)
* Add live model discovery for provider catalog

* Fix model discovery request headers

* fix(codex): sync live model limits with local catalog

* test(codex): split live model discovery coverage into dedicated route tests

* fix(codex): use chatgpt account id for live model sync

* Add GitHub-backed Codex model discovery fallback

* fix(providers): tighten oauth config tests and provider model display comments

* test: align client version expectations with release default

* fix(codex): keep discovery complexity within baseline

* fix: rebase live Codex model discovery onto release/v3.8.47, preserving kimi-web buildHeaders (#6308)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-11 05:05:27 -03:00
Diego Rodrigues de Sa e Souza
fa7eb61a05 fix(i18n): backfill 194 missing pt-BR keys (#6695) (#6723)
* fix(i18n): backfill 194 missing pt-BR keys and add key-parity regression test (#6695)

* Merge branch 'release/v3.8.47' into fix/6695-i18n-drift

Resolve i18n key-parity and CHANGELOG-fragment conflicts:
- Convert the #6695 CHANGELOG.md bullet to a changelog.d/ fragment
  (the fragment convention landed on release/v3.8.47 after this PR
  branched, per changelog.d/README.md).
- Backfill 61 additional pt-BR keys that entered en.json on
  release/v3.8.47 after this PR's original 194-key backfill, so the
  PR's own key-parity regression test (tests/unit/i18n-pt-br.test.ts)
  stays green against the moving release baseline.
2026-07-11 05:04:42 -03:00
Diego Rodrigues de Sa e Souza
2c413f2b75 feat: sidebar search/filter input (#4013) (#6810)
* feat(dashboard): add search/filter input to the dashboard sidebar (#4013)

Adds a search box at the top of the expanded sidebar that filters nav
sections/groups/items client-side by label, so users don't have to
hunt through the growing nav tree. Reuses the existing common.search /
common.noResults i18n keys (no new locale edits needed) and the shared
Input icon="search" pattern. Matching sections auto-expand while
searching and the accordion/pin state is restored once the query is
cleared.

Filtering logic is extracted into a pure filterSidebarSectionsByQuery()
helper (src/shared/utils/sidebarSearch.ts) so it is trivially unit
testable independent of React/next-intl/next-navigation.

* fix(test): move Sidebar.search test to a runner-collected path (test-discovery gate)
2026-07-11 05:04:13 -03:00
Diego Rodrigues de Sa e Souza
9159b286d0 feat: per-model web-search interception rule (#3384) (#6814)
* feat(routing): per-model web-search interception rule (#3384)

Adds a per-provider/per-model interceptSearch rule (src/lib/db/interceptionRules.ts,
key_value namespace interception_rules) that overrides the existing native
web-search bypass defaults (Codex/Gemini/Claude->Claude passthrough) in
webSearchFallback.ts. Wired at the existing prepareWebSearchFallbackBody() call
site in chatCore.ts. Resolution precedence: per-model rule > provider-level rule
> existing native-bypass defaults.

This lands Phase 1-2 of the plan (rule store + search interception). Web-fetch
interception and the dashboard UI toggle are tracked as follow-up phases.

* fix(db): register interceptionRules in localDb re-export layer (db-rules gate)

* fix(db): renumber interception_rules migration 119→120 (collision with model_capability_overrides)
2026-07-11 05:03:56 -03:00
Diego Rodrigues de Sa e Souza
d11fd9380b docs: refresh stale llm.txt facts + relocate design.md to docs/architecture/DESIGN_SYSTEM.md (#6849)
* docs: refresh stale llm.txt facts + move design.md to docs/architecture/DESIGN_SYSTEM.md

llm.txt was frozen at the v3.8.8 era (177 providers, 37 MCP tools, 14
strategies, 9-factor scoring, 75% coverage gate). Update every factual
claim to the current state (248 providers, 94 tools / 30 scopes, 18
strategies, 12-factor scoring, ratchet + 60% floor, TS 6, current docs/
layout) and re-sync the 42 exact-copy i18n mirrors.

design.md at the root was a standardization plan whose phases 1-6 all
shipped; rewrite its header as a permanent reference and relocate it to
docs/architecture/DESIGN_SYSTEM.md per the root-hygiene policy (root =
configs + canonical docs only).

* docs: add MDX frontmatter to DESIGN_SYSTEM.md (in-app docs pipeline requires it)
2026-07-11 04:57:32 -03:00
Diego Rodrigues de Sa e Souza
baad78e249 docs: rename /implement-prs → /merge-prs in Hard Rule #21 (skill renamed 2026-07-11) (#6847) 2026-07-11 04:33:24 -03:00
Diego Rodrigues de Sa e Souza
427ee244a3 fix(usage): honor xAI provider-reported exact cost (#6711)
OmniRoute's calculateCost() always estimated request cost from token
counts x static pricing, discarding xAI's exact provider-reported cost
when present. xAI's chat-completions usage object reports the precise
billed cost via cost_in_usd_ticks (docs.x.ai/developers/cost-tracking
and the API reference's usage schema: "TICKS_IN_USD_CENT: i64 =
100_000_000" => 1e10 ticks/USD, e.g. 37756000 ticks ~= $0.0038).

calculateCost()/computeCostFromPricing() now short-circuit to this
exact figure when present -- before any pricing DB lookup, so it also
works for models without a local pricing row -- and still fall back to
the token-based estimate when it is absent. The field is threaded
through both the streaming (extractUsage/normalizeUsage) and
non-streaming (extractUsageFromResponse) usage-extraction paths.

Corrected divisor vs upstream: the upstream PR used /1e12 (a 100x
under-report, e.g. reporting $0.00123 as the doc's $0.123 example);
this port uses the doc-verified /1e10 instead, confirmed against both
the cost-tracking guide and the API reference's usage-object schema.

Inspired-by: https://github.com/decolua/9router/pull/2453

Co-authored-by: ryanngit <74137224+ryanngit@users.noreply.github.com>
2026-07-11 04:33:09 -03:00
Diego Rodrigues de Sa e Souza
5e1a325e72 feat(combo): strict budget-cap fallback policy for auto/* combos (#3470) (#6816)
Auto-combo transparency + budget controls: the engine's budgetCap enforcement
always degraded to the globally cheapest candidate when every candidate
exceeded the cap - silently overspending instead of respecting the cap.

- engine.ts: budgetFallback "cheapest" (default, legacy) | "strict"
  (BudgetExceededError when no candidate fits budgetCap)
- requestControls.ts: X-OmniRoute-Budget-Fallback header +
  resolveRequestAutoControls() consolidating mode/budget/fallback parsing
- resolveAutoStrategy.ts / autoConfig.ts: thread combo-level
  config.budgetFallback and catch BudgetExceededError into an HTTP 402
- chat.ts: switch to the consolidated resolveRequestAutoControls() helper
  (net line reduction, stays under the frozen file-size baseline)

Regression guard: tests/unit/auto-combo-budget-fallback-3470.test.ts
2026-07-11 04:32:26 -03:00
Diego Rodrigues de Sa e Souza
d96bd80343 refactor(usage): type saveRequestUsage with UsageEntry interface + any-budget ratchet (#3512) (#6809)
Replace saveRequestUsage(entry: any) with a typed UsageEntry interface
mirroring the usage_history columns 1:1. Fields stay optional/nullable
since different writers (chatCore success/failure, rejected-request
accounting, Codex Responses WS) populate the row incrementally; tokens
stays unknown since callers pass either raw provider-shaped usage or
the normalized {input,output,cacheRead,...} shape.

Also cleaned the file's other any usages (getUsageHistory filter,
getUsageDb next-cursor cast, appendRequestLog tokens param,
getRecentLogs catch) so it now sits at zero any and can be added to
the check:any-budget:t11 zero-any allowlist.

Documents the DB-entity <-> TS-interface convention in
docs/architecture/CODEBASE_DOCUMENTATION.md Sec 11.
2026-07-11 04:32:10 -03:00
Diego Rodrigues de Sa e Souza
6d3d122b84 feat(dashboard): improve Provider Quota page horizontal density (#3520) (#6815)
QuotaCardGrid stacked every provider group vertically in a single
flex flex-col container, and each group's own card grid didn't go
multi-column until the md breakpoint. Provider groups now flow into a
2-column CSS multi-column layout on very wide (2xl) screens instead of
an unconditional vertical stack, and each group's card grid starts at
2 columns immediately, filling horizontal whitespace sooner on
narrower-but-not-mobile viewports.

Regression guard: tests/unit/quota-card-grid-horizontal-layout.test.ts
2026-07-11 04:31:52 -03:00
Diego Rodrigues de Sa e Souza
7fae224315 feat(providers): manual context-window override for custom models (#4125) (#6822)
Add a manual per-model "Context Window Override" so an operator can correct
a provider's misreported context length (e.g. reports 1M when the real
limit is 128K) instead of the model getting silently dropped from combo
routing once the wrong value lands in the catalog.

Reuses the existing Feature-5004 model_context_overrides table
(source="manual") — already the priority-0 source getModelContextLimit()
(the function combo's context-window filter calls) reads ahead of the
models.dev/registry/static catalog — so no new resolver logic was needed,
only the missing write path:

- PUT /api/provider-models now accepts an optional contextWindowOverride
  (number to set, null to clear), persisted via setModelContextOverride/
  removeModelContextOverride.
- GET /api/provider-models surfaces the current override value + source
  back on each custom-model row.
- CustomModelsSection.tsx: edit form gained a Context Window Override
  input + a badge on the model row when an override is set.

Regression guard: tests/unit/provider-models-context-window-override-4125.test.ts
(manual override wins over a misreported catalog value, GET round-trip,
clearing via null, default-unchanged behavior).
2026-07-11 04:31:35 -03:00
Diego Rodrigues de Sa e Souza
778c3aeab9 fix(kiro): probe IdC region during profileArn discovery, cross-region (recovers #6099) (#6840)
* fix(kiro): route Amazon Q runtime by profileArn region for cross-region IdC

Enterprise AWS IAM Identity Center accounts whose IdC instance lives outside the two
Amazon Q Developer profile regions (us-east-1 / eu-central-1) - e.g. eu-north-1
(Stockholm), start URL https://d-XXXX.awsapps.com/start - showed no limits and returned
502 on every request.

Root cause: the backend used the IdC/OIDC token region (providerSpecificData.region, e.g.
eu-north-1) for every CodeWhisperer runtime call, hitting q.eu-north-1.amazonaws.com - a
host that does not exist as a Q Developer runtime endpoint. Per AWS docs ("Supported
Regions for the Q Developer console and Q Developer profile"), the Q Developer *profile*
(which produces the profileArn and hosts generateAssistantResponse / GetUsageLimits /
ListAvailableModels / ListAvailableProfiles) is only hosted in us-east-1 and eu-central-1,
regardless of the IdC region; "data is stored in the Region where you create the Amazon Q
Developer profile."

Fix (new open-sse/services/kiroRegion.ts) decouples the two regions:
- providerSpecificData.region stays the IdC/OIDC region, used ONLY for
  oidc.{region}.amazonaws.com token mint/refresh.
- The runtime region is derived from the profileArn (resolveKiroRuntimeRegion):
  profileArn region -> a valid stored profile region -> us-east-1. A stored IdC region
  that is not a Q profile region (eu-north-1) is ignored for runtime.
- Profile discovery (discoverKiroProfileArnAcrossRegions) probes the Q profile regions
  (EU IdC -> eu-central-1 first) with the cross-region SSO token instead of q.{idcRegion}.

Wired into: executors/kiro.ts (generateAssistantResponse targets the profile region),
services/usage/kiro.ts (getKiroUsage multi-region discovery + profileArn runtime region so
Limits resolves), services/kiroModels.ts (ListAvailableModels), and
src/lib/oauth/providers/kiro.ts (login-time postExchange profile discovery).

Adds tests/unit/kiro-idc-cross-region.test.ts (15 cases). All Kiro suites pass (60 tests).

* fix(kiro): probe the IdC region too during profileArn discovery (any IdC region)

Make profile discovery general for an IdC in ANY of the ~30 IdC-supported AWS regions
(us-west-2, ap-southeast-2, me-central-1, af-south-1, ...), not just eu-north-1.

buildKiroProfileDiscoveryRegions now probes the two documented Q Developer profile regions
FIRST (us-east-1 / eu-central-1, EU-first for EMEA IdC regions to cut latency), then appends
the IdC/stored region itself as a forward-compatible fallback: if AWS ever co-locates the
profile with the IdC or expands the profile-region list, a same-region probe still finds it.
Probing a region with no profile simply returns nothing and we fall through. The profileArn's
own region remains authoritative for every runtime call (resolveKiroRuntimeRegion), so a
newly-issued ARN in any region is honored automatically.

Adds ap-southeast-2 (APAC) cross-region coverage and updates the discovery-order tests.

---------

Co-authored-by: artickc <artur1992123@mail.ru>
2026-07-11 04:25:47 -03:00
Diego Rodrigues de Sa e Souza
112b1499df fix(antigravity): sanitize Cloud Code safety settings (#6839)
Co-authored-by: kfiramar <83420275+kfiramar@users.noreply.github.com>
2026-07-11 04:25:32 -03:00
Diego Rodrigues de Sa e Souza
a3e38a2c0c docs(readme): fix stale strategy/tool/scoring counts (#6853)
README still claimed 17 routing strategies (the table was missing
pipeline), 95 MCP tools, and 9-factor Auto-Combo scoring. Align with
the source (ROUTING_STRATEGY_VALUES has 18 entries) and the canonical
docs (MCP-SERVER.md: 94 tools; AUTO-COMBO.md: 12-factor).
2026-07-11 04:03:52 -03:00
Ray Doan
691dae7079 feat(proxy): implement latency-optimized proxy rotation strategy (#6798)
* feat(proxy): implement latency-optimized proxy rotation strategy

Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR) and the direct CHANGELOG.md edit (fragments-first);
the author's env/docs/i18n deltas were re-applied cleanly onto the release tip.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(proxy): add latency-rotation env var to .env.example

PROXY_LATENCY_WINDOW_HOURS was referenced in src/lib/db/proxies.ts and
documented in docs/reference/ENVIRONMENT.md, but missing from
.env.example, tripping the env/docs sync gate.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* chore(changelog): re-sync CHANGELOG.md to release tip

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* refactor(proxy): extract latency-strategy helpers to keep frozen files under cap

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* test(db-rules): expect 35 audited modules (proxyLatency joins INTENTIONALLY_INTERNAL)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-11 03:47:10 -03:00
lunkerchen
b6ab8ba1ec feat(i18n): add Traditional Chinese (zh-TW) localization for frontend and CLI (#6320)
* feat(i18n): add Traditional Chinese (zh-TW) localization for frontend and CLI

- Add src/i18n/messages/zh-TW.json translating frontend web UI
- Add bin/cli/locales/zh-TW.json translating CLI commands and descriptors
- Register zh-TW in config/i18n.json and docs/guides/I18N.md
- Update scripts/i18n/generate-multilang.mjs matching the new locale setup

* fix: update i18n locale count from 42 to 43 after adding zh-TW

The docs strict checker (check-docs-counts-sync.mjs) validates that
README.md and I18N.md reflect the real locale count. Adding zh-TW
bumped the count from 42 → 43.

* fix(i18n): translate providers free-filter labels in zh-TW (#6694 guard)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: lunkerchen <lunkerchen@users.noreply.github.com>
2026-07-11 02:15:44 -03:00
Xiangzhe
dfe064861e Clamp reasoning token buffer to model output cap (#6714)
* fix(combo): clamp reasoning buffer to model output cap

* fix(routing): preserve near-cap reasoning max tokens

* fix(routing): getExplicitModelOutputCap falls through to registry cap on non-numeric synced limit_output

getExplicitModelOutputCap short-circuited to null whenever a synced
capability row existed, even if that row's limit_output was not a number
(models.dev commonly omits it). That silently disabled the reasoning-token
buffer clamp for any model with a synced row lacking an output limit.

Now only return the synced value when it IS a number; otherwise fall
through to registryModel.maxOutputTokens / spec.maxOutputTokens, matching
the ??-chain precedence already used by getResolvedModelCapabilities().

Adds a standalone regression test (proves the fallthrough returns the real
registry cap, not null) and hardens the #6274 fixture id so its no-output-cap
case does not prefix-match the real glm-5.2 static spec.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* chore(stryker): register ollama-quota covering tests (release drift from merge burst)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-11 02:10:56 -03:00
Diego Rodrigues de Sa e Souza
6f749fef9a docs(changelog): reconcile 3-day merge burst — 16 fragments, 4 promised credits, contributors hall 32→63
- changelog.d fragments for the 20 merged PRs that landed without a bullet
  (#6072 #6308 #6323 #6538 #6556 #6586 #6611 #6647 #6675 #6698 #6757 #6759
  #6804 #6821 + ci rollup #6781/#6691/#6693 + docs rollup #6643/#6644/#6646/#6663;
  omniglyph bump #6661 folded into the #6556 bullet)
- deliver the 4 credits promised in close comments but never written:
  @alltomatos (#6819 dup of #6721), @samimozcan (#6762/#6753 subsumed by #6790),
  @chirag127 (#6756 dup of #6757), @Squawk7777 (#6565 dup of #6564 — appended to
  the existing #6564 bullet; changelog-integrity flags that edit as a removal,
  intentional: ALLOW_CHANGELOG_REMOVALS justification)
- rebuild the v3.8.47 Contributors hall from merged-PR authors + thanks credits
  + prior hall: 32 → 63 contributors
2026-07-11 02:10:15 -03:00
backryun
c92bdd13c6 fix(lmarena): modernize Arena web provider + static Direct-chat catalog (#6280)
* fix(lmarena): modernize Arena web provider + static Direct-chat catalog

Update the lmarena provider for arena.ai (product rebranded from LMArena):

- Route chat via arena.ai create-evaluation with Chrome TLS impersonation
  (tls-client-node) and optional browser-minted recaptchaV3Token.
- Seed Text+Search (48) into the chat registry; seed Image (27) only into
  IMAGE_PROVIDERS. Disable live HTML model discovery; resolve public names to
  Arena UUIDs from the static TypeScript allowlist (no scrape JSON in-repo).
- Soft-exclude 404/502 model ids; slow/stop bulk test-all probes for this provider.
- Do not fold IMAGE_PROVIDERS/video specialty into the chat provider catalog when
  a chat registry already exists (lmarena/openai/xai).
- Display name Arena (Free); keep wire id `lmarena` / alias `lma` for back-compat.
- Theme-aware provider icons: arena-light.svg / arena-dark.svg.
- Preserve split Supabase SSR cookie reconstruction for arena-auth-prod-v1.*.

* fix(providers): align provider-models-route test fixture + regen provider reference

Fold the topaz image-only catalog entry's apiFormat/supportedEndpoints
into the local-catalog test fixture (route now tags media-only
providers per the lmarena PR's staticModels.ts change), regenerate
PROVIDER_REFERENCE.md against the merged release providers.ts, and add
the changelog fragment for #6280.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* test: align web-cookie fallback suite — lmarena now has a registry entry (probe path)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-11 01:33:24 -03:00
NOXX - Commiter
0a7b58b20f \ feat: operator-configurable account rotation\ (#6763)
* feat(resilience): operator-configurable account rotation

Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR); the author's accountFallback/.env deltas were
re-applied cleanly onto the release tip.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* docs(env): document configurable account-rotation env vars in ENVIRONMENT.md

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* refactor(rotation): extract rotation gate/context helpers to keep accountFallback.ts under frozen cap

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* chore(changelog): re-sync CHANGELOG.md to release tip (restore lost base bullet)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* test(stryker): register rotation-config test in tap.testFiles for mutation coverage

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* test(stryker): register ollama-quota covering tests (drift from #6731/#6817/#6742) + re-sync

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
2026-07-11 01:05:07 -03:00
Xiangzhe
65890d4aab fix(logs): prevent stale detail refresh reopening modal (#6323)
* fix(logs): prevent stale detail refresh reopening modal

* chore(stryker): register ollama-quota covering tests (release drift from merge burst)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-11 01:03:17 -03:00
Septianata Rizky Pratama
45310f202d fix: auto-start WS server in-process and change default port to 20132 (#6072)
* feat: change default LIVE_WS_PORT from 20129 to 20132

Update the default WebSocket port for the live dashboard server from 20129 to 20132 across all configuration files, documentation, code comments, and tests. Also consolidate OMNIROUTE_DISABLE_LIVE_WS and OMNIROUTE_ENABLE_LIVE_WS into a single OMNIROUTE_ENABLE_LIVE_WS flag. Wire the live WebSocket server to start in-process via instrumentation-node.ts.

* feat: clarify NEXT_PUBLIC_LIVE_WS_PUBLIC_URL path usage and derive upgrade path from URL

Update .env.example and ENVIRONMENT.md to document that the pathname portion of NEXT_PUBLIC_LIVE_WS_PUBLIC_URL (e.g. /live-ws) is used as the WebSocket upgrade path by the dev proxy, handshake response, and client connection logic.

Extract deriveLiveWsPath() into shared/utils/wsPath.ts and wire it through:
- src/app/api/v1/ws/route.ts — handshake response path field
- src/hooks/useLiveDashboard.ts — build

* fix: use the standard URL API to safely parse and update the effectiveWsUrl

* build(docker): expose live WebSocket server port and configure CORS origins

Add LIVE_WS_PORT (20132), LIVE_WS_HOST (0.0.0.0), and LIVE_WS_ALLOWED_ORIGINS environment variables to all Docker Compose profiles and expose the WebSocket port mapping. Prevent infinite self-loop in standalone-server-ws.mjs by skipping proxy when the server itself is running on the LiveWS port.

* docs(env): fix comment formatting for HOST and HOSTNAME variables

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-11 01:01:27 -03:00
Diego Rodrigues de Sa e Souza
a42f993645 chore(stryker): register ollama-quota covering tests (merge-burst drift, v3.8.47)
The 3 covering unit tests from #6731/#6817/#6742 (issue-6638-ollama-quota,
ollama-cloud-weekly-quota-cooldown-3709, issue-6686-quota-preflight-coverage)
exist on release but were never added to tap.testFiles when those PRs merged.
Completes the registration so mutant kills count; unblocks every PR touching a
mutated module. Part of the owner-approved merge-burst drift cleanup.
2026-07-10 23:16:28 -03:00
Diego Rodrigues de Sa e Souza
a25175b608 chore(quality): rebaseline complexity 2053->2054 (merge-burst drift, v3.8.47)
Inherited drift from today's /implement-prs merge burst (~36 PRs). check:complexity
does not run on the PR->release fast-path, so the branch accrued +1 unmeasured. No
orphan/feature PR introduces a NEW violation (complexity-net-zero); the only flagged
function is the pre-existing getResolvedModelCapabilities. Owner-approved rebaseline
to unblock the FQG of ~7 green-except-complexity orphans.
2026-07-10 21:49:02 -03:00
Diego Rodrigues de Sa e Souza
039acabad9 fix(providers): Kiro adaptive-thinking allowlist excludes sonnet-4.5/haiku-4.5 (#6576) (#6726)
* fix(providers): Kiro adaptive-thinking allowlist excludes sonnet-4.5/haiku-4.5 (#6576)

* chore(6726): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first)

* test(kiro): migrate selector-strip test to claude-sonnet-5 (only Kiro adaptive-thinking model, #6576)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-10 19:54:17 -03:00
Diego Rodrigues de Sa e Souza
2e3508186a feat(resilience): weekly-429 cooldown for fetcher-less providers (#3709) (#6817)
* feat(resilience): weekly-429 cooldown for fetcher-less providers (#3709)

Ollama Cloud free-tier accounts have a hard WEEKLY request cap. On cap the
upstream returns 429 "you (<account>) have reached your weekly usage
limit", but ollama-cloud is an apikey-category provider, so the existing
oauth-only shouldUseQuotaSignal gate in checkFallbackError skips the
subscription-quota-text classifier (Issue #2321) for its 429s -- the
account fell through to the generic exponential backoff (~1s, capped at
2min) and got retried every few minutes for the rest of the week (one
account took 285x429 in 48h).

Adds a new, ungated weekly-usage-limit text classifier that applies a 24h
QUOTA_EXHAUSTED cooldown regardless of provider category. Extracted the new
classifier -- together with the existing #2321 subscription-quota logic --
into a new open-sse/services/quotaTextCooldowns.ts module so the frozen
accountFallback.ts (file-size-baseline cap) didn't have to grow; net effect
shrinks accountFallback.ts by 20 lines.

This is Phase A of the plan (open-sse/services/accountFallback.ts:1038-1045
"weekly-429 cooldown"); Phase B (generic local request-counter preflight for
manual provider_plans dimensions) is a separate, larger follow-up per the
plan's own phasing.

* chore(6817): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first)
2026-07-10 19:50:38 -03:00
Diego Rodrigues de Sa e Souza
33ca6caef3 fix(resilience): apikey-provider 429s honor explicit quota-exhausted text (#6638) (#6731)
* fix(resilience): apikey-provider 429s honor explicit quota-exhausted text (#6638)

Ollama Cloud (and any other apikey-category provider) 429s skipped body-text
quota classification entirely; a genuine multi-day quota exhaustion was
misclassified as a plain rate_limit_exceeded with a few seconds of cooldown,
so combo routing retried the account immediately. shouldPreserveQuotaSignals()
now lets an explicit quota-exhausted signal (looksLikeQuotaExhausted) override
the apikey-category default, and parseDayGranularityResetMs() adds day-
granularity reset-hint parsing ("...reset in 3 days.") alongside the existing
Xh/Ym/Zs parsing.

Regression guard: tests/unit/issue-6638-ollama-quota.test.ts (RED before the
fix, GREEN after). Aligned two tests/unit/account-fallback-service.test.ts
cases that had codified the old buggy behavior for apikey-provider quota
text.

* chore(6731): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first)
2026-07-10 19:47:05 -03:00
Diego Rodrigues de Sa e Souza
249462d1ff fix(resilience): route remaining credential-selection call sites through quota preflight (#6686) (#6742)
* fix(resilience): route remaining credential-selection call sites through quota preflight (#6686)

* chore(6742): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first)
2026-07-10 19:45:06 -03:00
Diego Rodrigues de Sa e Souza
2ef88763e4 feat(xai): route xAI clients to Grok native /v1/responses endpoint (#6709)
* feat(xai): route xAI clients to Grok native /v1/responses endpoint

xAI ships a native /v1/responses endpoint (https://api.x.ai/v1/responses)
alongside /v1/chat/completions, but XaiExecutor extended BaseExecutor
without overriding buildUrl(), so every request always resolved to the
static chat-completions baseUrl regardless of target format — the last
genuinely-missing slice of decolua/9router#2439 (grok-build-0.1, the
reasoning-effort suffix routing, and bare grok-* routing were already
ported in prior cycles).

Add responsesBaseUrl to the xai registry entry and tag
grok-4.20-multi-agent-0309 (upstream's own Responses-only id) with
targetFormat: "openai-responses", mirroring the existing model-tag-driven
routing pattern already used by the gh executor (9router#102) and the
"openai" -pro heuristic in open-sse/executors/default.ts — the per-model
registry tag is the single source of truth that also drives chatCore's
body translation, so URL and body stay in lockstep. XaiExecutor.buildUrl
now checks getModelTargetFormat("xai", model) and resolves to the native
Responses endpoint only for tagged models, leaving every other grok-*
model on the existing chat-completions bridge.

TDD: tests/unit/executor-xai.test.ts adds a RED-then-GREEN case asserting
grok-4.20-multi-agent-0309 resolves to https://api.x.ai/v1/responses and
a control case asserting grok-4.3 still resolves to
https://api.x.ai/v1/chat/completions.

Co-authored-by: ryanngit <74137224+ryanngit@users.noreply.github.com>
Inspired-by: https://github.com/decolua/9router/pull/2439

* chore(6709): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first)

---------

Co-authored-by: ryanngit <74137224+ryanngit@users.noreply.github.com>
2026-07-10 19:44:06 -03:00
Diego Rodrigues de Sa e Souza
28fcd418a4 feat: request count log per provider, per date (#4009) (#6812)
* feat(dashboard): request count log per provider, per date (#4009)

Some providers bill by request rather than by token, so operators need
a plain per-provider, per-date request count breakdown, not just token
aggregates. Adds a new getProviderDailyUsageRows() aggregation query
(src/lib/db/usageAnalytics.ts), a dedicated GET
/api/usage/requests-by-provider-date route (kept separate from the
frozen /api/usage/analytics route to respect the file-size baseline),
and a sortable, single-date-filterable table on Dashboard -> Analytics.

Closes #4009

* chore(6812): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first)
2026-07-10 19:40:18 -03:00
Diego Rodrigues de Sa e Souza
e8fce67e70 feat(dashboard): add search to Playground model picker dropdown (#4086) (#6811)
* feat(dashboard): add search to Playground model picker dropdown (#4086)

The shared ModelSelectModal (combo builder + CLI-code cards) already had
search, but the Playground's raw model <select> in StudioConfigPane stayed
a flat unsearchable list - unusable once a provider like OpenRouter
contributed 50+ models.

Adds a search input above the dropdown that filters options via
filterModelsByQuery() (Turkish-safe accent/case-insensitive match, reusing
matchesSearch()). The currently selected model always stays pinned in the
list even when it doesn't match the query, so typing never silently swaps
the active selection. Reuses the existing common.search i18n key already
translated in all 42 locales - no new key needed.

* chore(6811): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first)
2026-07-10 19:40:13 -03:00
Diego Rodrigues de Sa e Souza
6904484f51 fix(translator): defer content_block_start until GLM streams the tool name (#6730)
* fix(translator): defer content_block_start until GLM streams the tool name (port from 9router#2077)

GLM 5.2 (and similar OpenAI-compatible upstreams) stream a tool call's id and
function.name across separate SSE delta chunks. The openai-to-claude streaming
translator emitted content_block_start immediately on the id-only chunk with an empty
name; the Claude SSE protocol cannot patch a block after emission, so the later
name-only chunk was dropped and Claude Code rejected the tool_use with an empty tool
name / "No such tool available:". Defer content_block_start until the name arrives
(start on args if they arrive first), and emit a start for any orphaned id-only tool
call at finish so content_block_stop is never orphaned.

Reported-by: itiwant (https://github.com/decolua/9router/issues/2077)

* chore(6730): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first)
2026-07-10 19:39:56 -03:00
Diego Rodrigues de Sa e Souza
0969b56951 fix(translator): strip empty cloud_base_branch from Cursor Subagent tool call (#6729)
* fix(translator): strip empty cloud_base_branch from Cursor Subagent tool call (port from 9router#2446)

The Responses->Chat tool-arg cleanup (stripEmptyOptionalToolArgs) only stripped
empty-string/empty-array optional args for Claude Code's Read tool. Cursor's local
Subagent tool call therefore passed through with the cloud-only field
cloud_base_branch: "", which Cursor rejects ("cloud_base_branch may only be specified
when environment equals cloud") before starting the subagent. Extend the cleanup to an
allowlist of Read + Subagent; arbitrary tools stay untouched.

Reported-by: like3213934360-lab (https://github.com/decolua/9router/issues/2446)

* chore(6729): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first)
2026-07-10 19:39:50 -03:00
Diego Rodrigues de Sa e Souza
2a52c402ce fix(antigravity): surface aborted Gemini tool calls off end_turn (#6713)
* fix(antigravity): surface aborted Gemini tool calls off end_turn

Gemini/Antigravity aborts a turn with finishReason MALFORMED_FUNCTION_CALL
(or a sibling like UNEXPECTED_TOOL_CALL) instead of completing cleanly. Both
Claude-facing translators collapsed these to a clean end_turn, hiding the
aborted tool call as a successful completion:
- the OpenAI hub path (openai-to-claude.ts convertFinishReason default), and
- the DIRECT Gemini->Claude path (gemini-to-claude.ts), which is the one
  Claude Code actually hits through an antigravity/Gemini-routed model.
Add isAbortFinishReason() to finishReason.ts and map these reasons to
tool_use on both paths; genuinely unknown reasons still fall back to end_turn.

Co-authored-by: anhdiepmmk <n08ni.dieppn@gmail.com>
Inspired-by: https://github.com/decolua/9router/pull/2462

* chore(6713): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first)

---------

Co-authored-by: anhdiepmmk <n08ni.dieppn@gmail.com>
2026-07-10 19:39:44 -03:00
Diego Rodrigues de Sa e Souza
b6e42651c0 fix(volcengine): clamp Kimi max_tokens to Ark endpoint cap (#6712)
* fix(volcengine): clamp Kimi max_tokens to Ark endpoint cap

VolcEngine Ark's Kimi coding-plan endpoint (ark.cn-beijing.volces.com)
enforces max_tokens <= 32768 server-side and returns 400 "integer above
maximum value, expected a value <= 32768" for anything over that ceiling.
OmniRoute's StripRule only supported dropping params outright, with no
numeric clamp mechanism, so a client sending a larger max_tokens (common
default, e.g. 65536) 400s outright against volcengine's kimi-k2-5-260127.

The 32768 cap is independently confirmed against two live-endpoint bug
reports hitting this exact Ark endpoint for both kimi-k2.5 and
kimi-k2.7-code (NousResearch/hermes-agent#51773, MoonshotAI/kimi-cli#1124),
not just upstream's own value — same cap upstream 9router#2460 uses.

StripRule gains two optional fields: `clampToModelMaxOutput` (clamp to the
model's own catalog maxOutputTokens ceiling, when set) and `maxOutputCap`
(a fixed endpoint-imposed ceiling); when both apply, the lower wins. The
new rule is scoped to the literal id `kimi-k2-5-260127` (OmniRoute's real
volcengine Kimi model, not upstream's `Kimi-K2.7-Code`), not a broad
/kimi/i regex, so it can never clamp an unrelated future Kimi listing
whose Ark cap may differ. glm-4-7-251222 (the other volcengine model) is
unaffected.

Inspired-by: https://github.com/decolua/9router/pull/2460
Co-authored-by: whale9820 <whale9820@users.noreply.github.com>

* chore(6712): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first)

---------

Co-authored-by: whale9820 <whale9820@users.noreply.github.com>
2026-07-10 19:39:35 -03:00
Diego Rodrigues de Sa e Souza
3f457cc77b fix(codex): surface capacity errors embedded in 200-OK SSE streams (#6710)
* fix(codex): surface capacity errors embedded in 200-OK SSE streams

Codex sometimes answers with HTTP 200 and a text/event-stream body whose
payload carries a transient error mid-stream (e.g. "Selected model is at
capacity...", server_is_overloaded, service_unavailable_error). Because the
outer HTTP status was 200, this looked like a successful response to every
caller — no retry, no circuit breaker, and no combo/account fallback ever
engaged, so a healthy account sat idle while the request silently failed or
truncated.

Add peekCodexSseTransientError() to open-sse/executors/codex.ts: it peeks the
first bytes of a text/event-stream Codex response, pattern-matches the known
transient-error signatures, and converts a match into a real 503 Response via
errorResponse() (Hard Rule #12 — sanitized, never raw upstream text). A 503 is
already a recognized provider-failure status in accountFallback.ts, so combo
routing and connection cooldown pick it up automatically. When no error
signature is found, the peeked prefix is prepended back onto the remaining
upstream body so the passthrough stays byte-identical to the unmodified
response.

Regression guard: tests/unit/codex-sse-capacity-fallback.test.ts — a
model-at-capacity payload and a server_is_overloaded/service_unavailable_error
payload both convert to 503; a normal single-chunk SSE stream and one split
across multiple network chunks both reassemble byte-for-byte unchanged.

Inspired-by: https://github.com/decolua/9router/pull/2452 (sub-bug #3 only —
OmniRoute already covers PR #2452's other two sub-bugs: service_tier "fast"
normalization and reasoning_effort "max" normalization).

Co-authored-by: ryanngit <74137224+ryanngit@users.noreply.github.com>

* chore(6710): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first)

---------

Co-authored-by: ryanngit <74137224+ryanngit@users.noreply.github.com>
2026-07-10 19:39:23 -03:00
Diego Rodrigues de Sa e Souza
4a9e36616b fix(sse): skip thinkingConfig for gemma models in openai→gemini translation (#6708)
open-sse/translator/request/claude-to-gemini.ts already guards against
sending thinkingConfig for gemma-4-* models (Gemma doesn't support it —
Vertex returns 400: "Thinking budget is not supported for this model"),
but the OpenAI-shape path (openai-to-gemini.ts) lacked the same guard, so
OpenAI-shape clients hitting a vertex gemma-4-* model still got a 400.

Mirrors the existing claude-to-gemini.ts guard: wrap the reasoning_effort
and Claude-shape thinking.budget_tokens branches with a model.startsWith
("gemma-4") check. Branch 3 (default includeThoughts for modern Gemini
models) already excludes non-"gemini" model ids and needed no change.


Inspired-by: https://github.com/decolua/9router/pull/2480

Co-authored-by: chy1211 <31048289+chy1211@users.noreply.github.com>
2026-07-10 19:39:17 -03:00
Diego Rodrigues de Sa e Souza
82f78320e8 fix(oauth): avoid bare-email dedup of Codex OAuth logins (#6706)
* fix(oauth): avoid bare-email dedup of Codex OAuth logins

When an incoming Codex OAuth connection has no verifiable workspace/account
id, do not merge it into an existing row on email match alone — that
silently overwrote the other account's token pair. Require a matching
chatgptUserId (a stable per-account JWT id) before merging; otherwise
insert a distinct connection row.

Co-authored-by: lucasjustinudin <34107354+lucasjustinudin@users.noreply.github.com>
Inspired-by: https://github.com/decolua/9router/pull/2477

* chore(6706): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first)

---------

Co-authored-by: lucasjustinudin <34107354+lucasjustinudin@users.noreply.github.com>
2026-07-10 19:39:11 -03:00
Diego Rodrigues de Sa e Souza
b45d10ceea fix(sse): unwrap bare {function:{…}} tools in openai→claude translation (#6704)
* fix(sse): unwrap bare {function:{…}} tools in openai→claude translation

Some OpenAI-shape clients send a tool as a bare `{ function: {...} }`
object, omitting the spec-required `type: "function"` parent wrapper.
The tools-mapping in openai-to-claude.ts (~line 366) only unwrapped
`tool.function` when `tool.type === "function"` was ALSO true, so a
bare-function tool fell through to `toolData = tool` (the wrapper
itself, with no `.name`), producing an empty `originalName` and
silently dropping the tool from the translated request — worse than
a 400, since the caller has no signal the tool never made it
upstream. Unwrap `tool.function` whenever present, independent of
the parent `type` field. Regression guard:
tests/unit/openai-to-claude-bare-tool.test.ts.

Co-authored-by: Samir Abis <me@samirabis.com>
Inspired-by: https://github.com/decolua/9router/pull/2473

* chore(6704): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first)

---------

Co-authored-by: Samir Abis <me@samirabis.com>
2026-07-10 19:39:04 -03:00
Diego Rodrigues de Sa e Souza
0840722826 fix(api): emit reasoning_content on claude-web + v0-vercel-web SSE (#6662) (#6743)
* fix(api): emit reasoning_content on claude-web + v0-vercel-web /v1/chat/completions SSE (#6662)

* chore(6743): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first)
2026-07-10 19:38:58 -03:00
Diego Rodrigues de Sa e Souza
e9d677055e fix(api): Responses passthrough emits event-only SSE frames after filtering commentary output (#6561) (#6735)
* fix(api): Responses passthrough emits event-only SSE frames after filtering commentary output (#6561)

The #6199 commentary-drop `continue;` branches in stream.ts skipped the
data: line for a dropped commentary event but never cleared the
already-buffered event: line for the same frame, so the next blank line
flushed the stale event: line alone -- an event-only SSE frame that
crashes the OpenAI Python SDK's json.loads(). Both drop sites now call
clearPendingPassthroughEvent() before continue. The commentary-drop
decision was extracted into a new responsesCommentaryDrop.ts module so
the fix does not grow the frozen stream.ts.

* chore(6735): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first)
2026-07-10 19:38:52 -03:00
Diego Rodrigues de Sa e Souza
fe3f274986 fix(resilience): resolve fp-pinned combo account back to real connection id (#6696) (#6732)
* fix(resilience): resolve fp-pinned combo account back to real connection id (#6696)

* chore(6732): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first)
2026-07-10 19:38:47 -03:00
Diego Rodrigues de Sa e Souza
303e5b5330 fix(startup): lazy-import ioredis in rateLimiter to fix MCP ERR_MODULE_NOT_FOUND (#6559) (#6725)
* fix(startup): lazy-import ioredis in rateLimiter to fix MCP ERR_MODULE_NOT_FOUND (#6559)

* chore(6725): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first)
2026-07-10 19:38:41 -03:00
Diego Rodrigues de Sa e Souza
c9f43bab85 fix(providers): stop quota card re-sorting Codex/GLM bars by remaining % (#6687) (#6722)
* fix(providers): stop quota card re-sorting Codex/GLM bars by remaining % (#6687)

QuotaCardExpanded.tsx unconditionally re-sorted quotas by remaining
percentage via sortQuotasByRemaining(), discarding the deterministic
CODEX_QUOTA_ORDER/GLM_QUOTA_ORDER window order quotaParsing.ts's
sortCodexOrder()/sortGlmOrder() had already established. A new
hasFixedQuotaOrder() + resolveQuotaDisplayOrder() skip the re-sort for
providers with a fixed window order (codex, glm family), threading
providerId from QuotaCard.tsx through to the display layer.

Regression guard: tests/unit/quota-card-expanded-fixed-order-6687.test.ts

* chore(6722): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first)
2026-07-10 19:38:35 -03:00
Diego Rodrigues de Sa e Souza
8338d6a5e7 fix(providers): drop image_generation for Codex Spark models regardless of plan (#6651) (#6721)
* fix(providers): drop image_generation for Codex Spark models regardless of plan (#6651)

* chore(6721): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first)
2026-07-10 19:38:28 -03:00
Diego Rodrigues de Sa e Souza
1834ed366e fix(build): suppress Turbopack over-bundling warning from agentSkills generator (#6582) (#6720)
* fix(build): suppress Turbopack over-bundling warning from agentSkills generator (#6582)

generator.ts builds outputBase from a non-literal outputDir parameter, so
Turbopack's file-tracing analyzer can't narrow it and emits an "Overly broad
patterns" warning per entry point that imports the module (603 warnings on
v3.8.46, up from 379). The fs access is legitimate and bounded, so
next.config.mjs now suppresses this specific diagnostic via turbopack.ignoreIssue,
mirroring the existing webpack.ignoreWarnings precedent in the same file.

* chore(6720): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first)
2026-07-10 19:38:15 -03:00
Diego Rodrigues de Sa e Souza
e5c19f4a12 fix(startup): rename reasoningControls.ts to avoid webpack casing collision (#6584) (#6718)
* fix(startup): rename reasoningControls.ts to avoid webpack casing collision (#6584)

* chore(6718): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first)
2026-07-10 19:38:04 -03:00
Andrew Munsell
9d3a2528bc fix(cursor): use Agent CLI build id for x-cursor-client-version (#6795)
* fix(cursor): use Agent CLI build id for x-cursor-client-version

Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR); the author's .env.example/docs deltas were
re-applied cleanly onto the release tip.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* chore(changelog): re-sync CHANGELOG.md to release tip (restore #6701 bullet)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-10 19:04:51 -03:00
Xiangzhe
0ad07b4d91 feat(models): add capability override UI (#6727)
* feat(models): add capability override UI

Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR); renumbered the migration 118 -> 119 to resolve the
collision with 118_provider_param_filters.sql already on release/v3.8.47; the author's
i18n/localDb deltas were re-applied cleanly onto the release tip.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(6727): import model-capability-overrides DB fns directly (not via localDb barrel) to keep localDb under file-size cap; aligns with anti-barrel convention

* chore(db): satisfy known-symbols contract for modelCapabilityOverrides

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
2026-07-10 18:47:33 -03:00
Jon Bailey
91efacadd3 fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED (#6791)
* fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED

Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR); keeps only the author's changes.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* refactor(deepseek): extract done-terminator helper to keep frozen file under cap

Extracts the FINISHED-drain scheduler and finish-once guard added for
the [DONE] terminator fix (#6777) into a new
deepseek-web-done-terminator.ts module, so deepseek-web.ts stays under
its frozen line cap (1148). Behavior is unchanged.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Pitchfork-and-Torch <Pitchfork-and-Torch@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-10 18:37:05 -03:00
Andrew Munsell
647544b810 chore(cursor): add Grok 4.5 effort/fast model IDs (#6774)
Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR); keeps only the author's changes.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-10 18:08:59 -03:00
Diego Rodrigues de Sa e Souza
ec0381c453 fix(api): point CLI health command at /api/monitoring/health (#6677) (#6717)
* fix(api): point CLI health command at /api/monitoring/health (#6677)

bin/cli/commands/health.mjs called GET /api/health, a route that was
moved to /api/monitoring/health without updating the CLI; the top-level
/api/health handler never existed on disk (only degradation/ and ping/
sub-routes). Point runHealthCommand()/runHealthComponentsCommand() at
/api/monitoring/health and read its real payload shape
(activeConnections, circuitBreakers: {open,halfOpen,closed}, memoryUsage)
instead of the old nonexistent requests/breakers/cache/memory fields.

* chore(6717): re-sync onto release tip; move CHANGELOG entry to changelog.d fragment (fragments-first)
2026-07-10 18:06:00 -03:00
Jon Bailey
698a30ea36 fix(api): accept all catalog engines on compression PUT schema (#6792)
Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR). Resolved the release's OmniGlyph engine addition
additively (types.ts/compression.ts kept both 'relevance' and 'omniglyph') and extended
stackedPipelineStepSchema + STACKED_PIPELINE_ENGINE_INTENSITIES with the omniglyph branch
so the ENGINE_CATALOG-parity test passes.

Co-authored-by: Pitchfork-and-Torch <Pitchfork-and-Torch@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-10 18:05:55 -03:00
Andrew B.
f704d0c1d0 fix(providers): classify 404 as MODEL_NOT_FOUND to stop retry storm (#6829)
Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR) and the direct CHANGELOG.md edit (fragments-first);
the author's chatCore/errorClassifier deltas were re-applied cleanly onto the release tip.

Co-authored-by: Andrian B. <andrewbalanesq@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-10 18:05:51 -03:00
Xiangzhe
48df80e4a5 fix(providers): update SenseNova Token Plan support (#6330)
Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR); the author's constants/registry/snapshot deltas
were re-applied cleanly onto the release tip.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-10 18:05:46 -03:00
Andrew B.
f79302ccd9 fix(bootstrap): filter empty process.env values to prevent Docker env crash loop (#6828)
Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR) and the direct CHANGELOG.md edit (fragments-first);
keeps only the author's bootstrap change.

Co-authored-by: Andrian B. <andrewbalanesq@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-10 18:05:41 -03:00
Chirag Singhal
dd057f590e fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6769)
Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR); keeps only the author's changes.

Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-10 18:05:36 -03:00
Aoxiong Yin
4b7f4b1ee2 fix(codex): strip include from compact responses requests (#6805)
* fix(codex): strip include from compact responses requests

Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR); keeps only the author's changes.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(6805): move include-strip assertion to standalone test file to keep executor-codex.test.ts under frozen size cap

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-10 18:05:31 -03:00
WITALO ROCHA
92d9870507 fix(translator): read PDF/video file attachments for Gemini/Antigravity and Claude (#6790)
Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR); keeps only the author's translator + test changes.

Co-authored-by: Wital <witalorocha216@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-10 18:05:26 -03:00
Andrew Munsell
5846e6af35 feat(cursor): add Opus 4.8, Fable 5, and Sonnet 5 model families (#6779)
Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR); keeps only the author's cursor registry + test changes.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-10 18:05:20 -03:00
KooshaPari
7aa9e6bdec fix(db): break probe-failed/restore loop on large storage.sqlite (#6632)
Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR); keeps only the author's changes.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-10 15:39:13 -03:00
Ronaldo Davi
2e2c038934 fix(api): accept enableRenderers in RTK compression config schema (#6703) (#6757) 2026-07-10 15:18:54 -03:00
Diego Rodrigues de Sa e Souza
9cafb7eb7c fix(compression): reconcile outer vs per-engine token counts (#6488) (#6741)
* fix(compression): reconcile outer vs per-engine token counts on degenerate output (#6488)

Outer originalTokens/compressedTokens (real tiktoken counter over extracted
message text) diverged from engineBreakdown[0]'s counts (a crude
JSON.stringify(requestBody).length/4 estimate), worst on small/degenerate
inputs where JSON structural overhead dominates. A single-engine breakdown
entry represents the exact same before/after transformation as the overall
response, so reconcileSingleEngineTokens() now overwrites that one entry's
counts with the outer, more accurate figures; multi-step pipeline
breakdowns are left untouched.

* chore(6741): resolve release sync — CHANGELOG.md restored to release tip, entry moved to changelog.d fragment (fragments-first)
2026-07-10 15:08:30 -03:00
Ronaldo Davi
c837de6c98 fix(providers): honor explicit thinking.budget_tokens 0 in openai->gemini transform (#6813) (#6821)
The transform forwarded the Claude-style thinking.budget_tokens into
generationConfig.thinkingConfig.thinkingBudget, but the presence check was
truthy (&& thinking.budget_tokens). An explicit budget_tokens: 0 — the
natural way to disable thinking — is falsy, so it was dropped and the
request fell through to the default thinkingConfig injection, making the
model think despite an explicit request for zero. Use an explicit numeric
check so 0 is honored as thinkingBudget 0; includeThoughts is only set for
a non-zero budget.
2026-07-10 15:06:26 -03:00
Diego Rodrigues de Sa e Souza
de193f8b24 fix(cli): fall back to settings.json when Claude Code binary is unresolvable (#6701) (#6734)
getCliRuntimeStatus() only ever answered `installed` from binary resolution
(known install paths + where/which PATH search), so a stale PATH, moved
binary, or uncatalogued install method reported "not found" even when
~/.claude/settings.json proved the CLI was installed and used before —
regressing behind upstream 9router's checkClaudeInstalled(), which already
falls back to the settings file when where/which fails.

withSettingsFallback() (new src/shared/services/cliInstallFallback.ts, kept
out of the frozen cliRuntime.ts to respect its file-size ceiling) restores
that parity: only when the binary lookup's own reason is "not_found" (never
for deliberate security rejections like unsafe/relative env overrides or
symlink escapes) and the tool's settings file exists on disk.
2026-07-10 15:06:15 -03:00
Ronaldo Davi
23c4086a81 fix(api): raise provider apiKey cap for cookie-based web providers (#6715) (#6759) 2026-07-10 15:06:09 -03:00
Chirag Singhal
6105bc1713 feat(fusion): let judge use its own knowledge and override the panel (#6804)
The judge prompt said to write an answer 'grounded in that analysis',
implicitly capping output at the panel's union. When all panel members
miss or are collectively wrong on something, the judge should apply its
own reasoning as a full participant and override consensus, while keeping
an honesty guard against fabrication. Adds a regression test.

Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com>
2026-07-10 15:06:03 -03:00
Diego Rodrigues de Sa e Souza
63fb1cbfa6 fix(i18n): translate provider visibility/free-paid filter labels across 15 locales (#6694) (#6719) 2026-07-10 08:25:33 -03:00
Diego Rodrigues de Sa e Souza
58e2dea022 fix(resilience): release combo session-stickiness pin on a terminal/quality-rejected account (#6692) (#6733)
applySessionStickiness() gated the sticky pin only on 5h/weekly usage headroom,
which is orthogonal to account availability, so a credits_exhausted/banned/
expired/rate-limited connection (or a quality-validation-rejected 200) kept
being re-promoted forever, defeating failover for that conversation.
2026-07-10 08:24:49 -03:00
Diego Rodrigues de Sa e Souza
33786137d1 feat(oauth): accept 9router camelCase Codex export in bulk import (#6665) (#6697)
* feat(oauth): accept 9router camelCase Codex export in bulk import (#6665)

* fix(changelog): restore CHANGELOG bullets eaten by release sync

* fix(changelog): re-restore CHANGELOG bullet after further release sync

* fix(changelog): re-restore CHANGELOG bullet after further release sync

* fix(changelog): re-restore #6697 bullet after release sync (#6678 landed)

* fix(changelog): re-restore CHANGELOG bullet after further release sync

* fix(changelog): re-restore #6697 bullet after #6700 release sync

* fix(changelog): re-restore #6697 bullet after release sync

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(changelog): restore #6126 bullet eaten by ancestry merge; re-insert only #6697's own

* chore(sync): merge release tip + restore own CHANGELOG bullet

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* chore(sync): merge release tip + restore own CHANGELOG bullet

* chore(changelog): re-sync after release merge — preserve sibling bullets

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-10 07:28:37 -03:00
Diego Rodrigues de Sa e Souza
e310aa7e41 fix(api): close HEAD requests immediately instead of hanging (#6400) (#6608)
* fix(api): close HEAD requests immediately instead of hanging (#6400)

Next.js 16's App Router route-handler pipeline (send-response.js) already
skips piping a Response body for HEAD, but its page-rendering pipeline
(pipe-readable.js -> pipeToNodeResponse, used for every app-router page/layout
render, including the not-found boundary any unmatched path falls through to)
has no such check and always streams the full rendered body regardless of
method. Combined with Node's default keep-alive framing, this left some
clients unsure whether the (implicitly bodyless) HEAD response had actually
finished.

Add scripts/dev/head-response-guard.cjs, wired into both the dev/start custom
server (run-next.mjs) and the packaged standalone server
(standalone-server-ws.mjs) at the same tier as the existing
http-method-guard.cjs/peer-stamp.mjs wrappers: for every inbound HEAD request
it discards any body bytes the inner handler writes and forces
Connection: close once .end() is called, independent of route existence or
auth state.

Regression guard: tests/unit/head-request-closes-6400.test.ts

* chore(changelog): restore #6400 bullet before re-sync

* chore(sync): merge release tip + restore #6608 bullet

* chore(sync): merge release tip + restore #6400 bullet

* chore(changelog): re-sync after release merge — preserve #6574 rerank bullet

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-10 07:27:32 -03:00
Diego Rodrigues de Sa e Souza
16e5b4d444 fix(providers): register openrouter rerank provider (#6574) (#6681)
* fix(providers): register openrouter rerank provider (#6574)

* fix(changelog): restore CHANGELOG bullets eaten by release sync

* fix(changelog): re-restore CHANGELOG bullet after further release sync

* fix(changelog): re-restore CHANGELOG bullet after further release sync

* fix(changelog): re-restore CHANGELOG bullet after further release sync

* fix(changelog): correct CHANGELOG restoration (previous attempt had a script-path bug)

* fix(changelog): re-restore CHANGELOG bullet after further release sync

* fix(changelog): re-restore #6681 bullet after #6700 release sync

* chore(sync): merge release tip + restore own CHANGELOG bullet

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* chore(sync): merge release tip + restore own CHANGELOG bullet
2026-07-10 07:24:16 -03:00
Diego Rodrigues de Sa e Souza
50881c31d0 chore(release): merge-train — batch-validate queued PRs once, --admin with evidence (#6784)
Merges every queued PR into a throwaway detached worktree cut from origin/<base>,
runs the fast-gates parity suite ONCE on the final train tip, and prints the
evidence line that authorizes gh pr merge --squash --admin per member
(merge-gates.md §7). Conflicting PRs are ejected and reported, the train
continues. Never pushes, never merges PRs, never stashes.
2026-07-10 07:23:27 -03:00
Diego Rodrigues de Sa e Souza
1eb218f76a ci(quality): TIA impacted-run splits dashboard tests onto the tsx loader (closes #6787) (#6788)
The impacted branch ran every selected file under --import tsx/esm; the
canonical test:unit:ci:shard runs tests/unit/dashboard/** under --import tsx
(CJS transform, required for @lobehub/icons/es/* deep imports). Any PR whose
impact map reached a dashboard component false-redded with 'Unexpected token
export' (reproduced on unrelated PRs #6317 and #6335 the same evening). The
selection is now split by segment with loader parity.
2026-07-10 07:23:17 -03:00
Diego Rodrigues de Sa e Souza
15d08a86c6 ci(quality): shard unit fast-path 2→4 — halves the heaviest job's wall time (#6781) 2026-07-10 07:23:06 -03:00
Diego Rodrigues de Sa e Souza
1ddb102a90 feat(release): changelog.d/ fragments — eliminate the CHANGELOG merge-storm cascade (#6783)
* feat(release): changelog.d/ fragments — kill the CHANGELOG-eat merge-storm cascade

Every PR used to edit the same top lines of CHANGELOG.md (its bullet), so in a
merge-storm each merge conflicted every sibling (CHANGELOG-eat / DIRTY cascade),
forcing a re-sync push + full CI re-run per PR per merge — O(N^2) CI runs.

A PR now adds ONE new file under changelog.d/{features|fixes|maintenance}/ with its
bullet; two PRs never touch the same file. scripts/release/aggregate-changelog.mjs
(npm run changelog:aggregate) folds fragments into the living section and deletes
them at release reconciliation. check:changelog-integrity (already wired in the
merge-integrity CI job — zero workflow change) now also validates fragment
well-formedness. This PR dogfoods the convention: its own entry is a fragment.

* chore(changelog): fragment filename matches PR number (#6783)
2026-07-10 07:22:49 -03:00
Hamsa_M
1bc6da5318 feat(cli): add CLI tools for pi, omp, letta, codewhale and jcode (#6318)
* feat(cli): add CLI tools for pi, omp, letta, codewhale and jcode

* fix(build): resolve CI build and lint errors

* fix(cli): resolve merge conflicts, add tests, align error handling for cli-additions

Resolve duplicate codewhale key from base merge, add unit/integration
tests for omp/letta settings routes and the omp DB module, and align
omp-settings/letta-settings error handling with sanitizeErrorMessage()
+ the pattern used by sibling jcode/pi/codewhale routes in this PR.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* chore(quality): correct cliRuntime.ts file-size baseline to actual post-merge line count

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(changelog): re-restore #6318 bullet after release sync

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(merge): restore #6126 clinepass files reverted by release auto-resolve + baseline re-merge

The release sync's auto-resolve reverted sibling PR #6126's clinepass work
(registry, catalog, oauth constants, clineAuth.ts, token-refresh case, tests)
and the file-size baseline — all outside this PR's scope. Restored to the
release versions, re-applied only this PR's own baseline entries, restored the
#6126 CHANGELOG bullet (re-inserting only this PR's own).

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(db): re-export db/omp from localDb (check:db-rules #2)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(db): keep localDb.ts at the 800-line cap after the omp re-export

Folded the MemoryVecMeta type re-export into the memoryVec named-export block
(inline 'type' specifier) so adding the db/omp line stays within the new-file
cap.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(cli): reduce #6318 scope to omp + letta (pi/codewhale/jcode already shipped)

pi, codewhale, and jcode landed via a separate PR before this one was
reconciled — re-adding parallel versions of their catalog entries, routes,
dashboard card, and i18n strings would have been a straight regression
(duplicate "pi" key silently shadowing the release's own entry, orphaned
JcodeToolCard/BaseUrlSelect/ApiKeySelect/cliEndpointMatch UI files with no
release-side wiring, and unrelated formatting/refactor drift in
codewhale-settings/pi-settings/config-generator/routeGuard picked up along
the way).

This PR now ships only the two tools that are genuinely new: omp (Oh My Pi)
and letta. Both settings routes shell out to `which omp`/`which letta` to
detect the local install, so they're loopback-gated in
LOCAL_ONLY_API_PREFIXES (Hard Rules #15/#17) in addition to the shared
requireCliToolsAuth() guard every cli-tools route requires
(tests/unit/cli-tools-auth-hardening.test.ts) — neither route had the guard
wired in yet. cli-catalog-counts.test.ts is updated to the real cardinality
(8 agent entries / 32 total, since omp+letta are both category "agent";
pi/codewhale/jcode were always category "code" and are unaffected). The
integration tests for omp/letta now pass a Request object to GET/DELETE and
assert the 401-when-auth-required path, matching the pattern already used by
the codewhale/jcode sibling routes. complexity-baseline.json is back to the
release's 2053 (the #6318 rebaseline note is gone — dropping the duplicate
JcodeToolCard.tsx/BaseUrlSelect.tsx removed the violations it was covering);
file-size-baseline.json's cliTools.ts entry shrank 955->915 to match the
smaller real file. CHANGELOG bullet rewritten to describe only omp+letta,
with a note on why pi/codewhale/jcode aren't part of this PR; also restores
the Kiro External IdP bullet that a prior merge auto-resolve had dropped
from the living section.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* test(cli-tools): align cli-tools-schema registry count with omp+letta (30→32)

Second exact-count guard missed in the scope-reduction pass; same legitimate
alignment as cli-catalog-counts.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(cli-tools): omp entry needs docsUrl (CliCatalogEntrySchema requires it)

https://github.com/can1357/oh-my-pi — verified official repo.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* chore(quality): cliTools.ts frozen 915→916 (+1 omp docsUrl line, own growth)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* chore(changelog): restore base + re-insert #6318 bullet after release sync

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* chore(sync): merge release tip + restore own CHANGELOG bullet

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: hamsa0x7 <hamsa0x7@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-10 01:27:52 -03:00
Moseyuh333
eeec4d9e87 feat(chaos): big update - optimize, fix bugs, add features, enhance UX (#6728)
* feat(chaos): add Chaos Mode — multi-model parallel/collaborative execution

- New DB column chaos_mode_enabled on api_keys table
- API key create/PATCH routes support chaosModeEnabled toggle
- Core library src/lib/chaos/chaosConfig.ts for persistent config
- API routes: GET/PUT/DELETE /api/chaos/config
- Chaos execution POST /api/skills/collect/chaos with key auth
- Dashboard page at /dashboard/chaos with full config UI
- Sidebar entry in Agentic Features section
- Chaos mode toggle in API Key editor permissions panel
- i18n keys for chaos config (en.json)

* feat(chaos): big update — optimize, fix bugs, add features

=== Changes ===

1. NEW: src/lib/chaos/chaosExecutor.ts — shared execution engine
   - Removed ~150 lines of duplicate dispatch logic between two API routes
   - Single executeChaosRun() function used by both endpoints
   - Added concurrency limit (max 10 parallel requests)
   - Added proper TypeScript interfaces (ChaosRunInput, ChaosRunResult)
   - Added error logging throughout

2. FIX: src/app/api/skills/collect/chaos/route.ts
   - Was MISSING logger import (log.error was undefined at runtime)
   - Reduced from 388 lines → 142 lines by delegating to shared executor
   - Added maxTokens support in schema validation

3. REFACTOR: src/app/api/chaos/run/route.ts
   - Simplified to thin wrapper: auth + validate + delegate to executor
   - Added maxTokens support

4. ENHANCE: src/lib/chaos/chaosConfig.ts
   - Added maxTokens config field (256-128k, default 4096)
   - Persisted per-instance via settings table

5. ENHANCE: UI — ChaosConfigPageClient.tsx
   - Loads available providers from /api/models for dropdown autocomplete
   - Added datalist-based provider selector in overrides section
   - Added Max Tokens configuration input
   - Added expandable provider list showing all detected providers
   - Fixed duplicate override detection

* fix(chaos): fetch providers from /api/providers instead of /api/keys

* fix(chaos): remove dead code isOverrideDuplicate, fix maxTokens fallback to include global config

* fix(chaos): resetConfig now shows error on HTTP failure (was silent)

* feat(dashboard): Chaos Mode — multi-model parallel/collaborative execution

Splits the PR down to only the genuinely new Chaos Mode feature (drops the
duplicate Skill Collector/GitHub-discovery portion already shipped via
#6186). Replaces the loopback fetch() dispatch (hardcoded to the wrong port)
with the established in-process synthetic-Request/route-handler pattern used
by src/lib/batches/dispatch.ts, moves settings persistence off raw SQL, and
adds unit test coverage for chaosConfig, chaosExecutor and the 3 chaos API
routes.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(chaos): fix external Bearer-auth bypass and stale config cache in tests

validateApiKey() returns a plain boolean for both the deployment-time env key
and a DB-backed key, so branching on `keyInfo === true` in
verifyChaosKey() (src/app/api/skills/collect/chaos/route.ts) treated every
valid API key as having full env-key access, silently skipping the
chaosModeEnabled permission check entirely. Now always resolves through
getApiKeyMetadata() and only bypasses the per-key check for the synthesized
env-key record (id: "env-key").

Also exports invalidateChaosConfigCache() from chaosConfig.ts and wires it
into the route tests' resetStorage() — the in-process config cache was
surviving DB resets between tests, causing state to leak across cases.

Fixes CHANGELOG-eat from the release merge (re-inserted the Chaos Mode
bullet against the base CHANGELOG.md, verified additive via
check-changelog-integrity.mjs) and re-syncs against release/v3.8.47 tip.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* docs(changelog): Chaos Mode overhaul bullet referencing #6728 after release sync

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(merge): restore #6126 clinepass files reverted by release auto-resolve + baseline re-merge

The release sync's auto-resolve reverted sibling PR #6126's clinepass work
(registry, catalog, oauth constants, clineAuth.ts, token-refresh case, tests)
and the file-size baseline — all outside this PR's scope. Restored to the
release versions, re-applied only this PR's own baseline entries, restored the
#6126 CHANGELOG bullet (re-inserting only this PR's own).

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(dashboard): chaos client hook must not import the server Pino logger

useChaosConfigData ("use client") pulled @/sse/utils/logger → shared Pino →
logRotation/dataPaths → node:fs into the browser bundle, breaking next build
(Turbopack: Can't resolve 'fs') — caught by the DAST smoke's isolated build.
console.error matches every other dashboard client component.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* test(api-manager): align switch-count invariant with the extracted toggle components

The Self-service block now renders 4 inline switches; the #5731 quota-bypass
and #6728 chaos-access toggles were extracted into dedicated components. The
type="button" invariant is preserved AND extended: the test now also asserts
each extracted component's switches declare type="button".

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* chore(sync): merge release tip + restore own CHANGELOG bullet

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Moseyuh333 <Moseyuh333@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
2026-07-09 23:49:48 -03:00
Hamsa_M
a5c555b0de feat(icons): prioritize local SVG icons over LobeHub npm for faster rendering (#6317)
* feat(icons): prioritize local SVG icons over LobeHub npm for faster rendering

* docs(changelog): add #6317 local-icons New Features bullet

---------

Co-authored-by: hamsa0x7 <hamsa0x7@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
2026-07-09 23:20:14 -03:00
Jan Leon
6103fd7239 fix: Stabilize live dashboard WebSocket routing (#6335)
* fix(dashboard): allow anonymous WS handshake + public /api/health/ping

The live-dashboard WebSocket descriptor handshake (GET /api/v1/ws?handshake=1)
and the lightweight GET /api/health/ping liveness probe both 401'd for
unauthenticated callers, even though both are metadata-only reads intended
to be public. clientApiPolicy required a bearer/dashboard-session before the
WS route handler could even return its own wsAuth/protocol descriptor, and
/api/health/ping was never added to PUBLIC_READONLY_API_ROUTE_PREFIXES
despite its own docstring documenting it as "No auth required".

clientApiPolicy.evaluate() now allows an anonymous
{kind:"anonymous", id:"ws-handshake"} subject for GET/HEAD/OPTIONS on
/api/v1/ws?handshake=1 — the route handler still performs its own real
wsAuth/dashboard/API-key decision before opening the socket — and
/api/health/ping is now in PUBLIC_READONLY_API_ROUTE_PREFIXES.

Re-scoped from the original PR per review-group-prs analysis: the
overlapping hardcoded /live-ws path-derivation change (useLiveDashboard.ts,
ws/route.ts) is dropped here since it conflicts with #6072's different
(dynamic, env-derived) approach to the same problem; only the
non-overlapping auth-policy win ships in this PR.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* chore(changelog): resync CHANGELOG.md after merging release/v3.8.47

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: JxnLexn <JxnLexn@users.noreply.github.com>
2026-07-09 23:19:32 -03:00
Thiago Reis
3a28b3b5e8 feat: add Kiro API key authentication (#6587)
* feat(oauth): add Kiro long-lived API key auth (#6587)

New /api/oauth/kiro/api-key route + KiroService.validateApiKey let a
Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API
key instead of the interactive OAuth device flow, with live
per-account model discovery (ListAvailableModels, 5-minute cache)
layered over the existing static registry fallback.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(changelog): re-restore #6587 bullet after release sync

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(merge): restore #6126 clinepass files reverted by release auto-resolve + baseline re-merge

The release sync's auto-resolve reverted sibling PR #6126's clinepass work
(registry, catalog, oauth constants, clineAuth.ts, token-refresh case, tests)
and the file-size baseline — all outside this PR's scope. Restored to the
release versions, re-applied only this PR's own baseline entries, restored the
#6126 CHANGELOG bullet (re-inserting only this PR's own).

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* chore(quality): freeze public-creds FP — AWS region default in validateApiKey signature

Same class as the existing minimax fn-param FPs: CRED_KEY_RE matches the
apiKey: param annotation and captures the region default "us-east-1",
which is not a credential.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(kiro): keep hard-failure reject semantics + kill public-creds fn-param FP at the source

- getKiroUsage: exhausted non-auth attempts now REJECT with the last HTTP-status
  failure in the pre-#6587 format (usage-service-hardening relies on it); auth
  failures keep the soft social-auth message.
- validateApiKey: region default moved out of the parameter list (the
  check-public-creds CRED_KEY_RE matches the apiKey: annotation and flags any
  literal in the signature); drops the brittle line-keyed allowlist entry.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: strangersp <strangersp@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
2026-07-09 22:49:06 -03:00
NOXX - Commiter
ece4bf7b53 feat(kiro): support enterprise External IdP (Your organization) logins (#6363)
* feat(kiro): support enterprise External IdP ("Your organization") logins

Kiro's enterprise "Your organization" sign-in federates through the org's own
identity provider (e.g. Microsoft Entra ID) and produces an `external_idp`
token that is fundamentally different from AWS Builder ID / IAM Identity Center
(AWS SSO-OIDC, refresh token starts with `aorAAAAAG`) and the Google/GitHub
social flow. Its `~/.aws/sso/cache/kiro-auth-token.json` carries an org-IdP JWT
access token, an IdP refresh token, a per-tenant `tokenEndpoint`, a public
`clientId` (no secret) and `scopes` (`codewhisperer:conversations …`).

Before this change every import path rejected these tokens (the
`aorAAAAAG` format gate + no client secret), and the runtime/quota calls would
have failed even if imported, so organization accounts could not be used.

This adds full external_idp support:

- New `open-sse/services/kiroExternalIdp.ts`: public-client refresh_token grant
  builder (`buildExternalIdpRefreshParams`), a token-endpoint SSRF allowlist
  (`validateExternalIdpTokenEndpoint` — Microsoft/Okta/Auth0/OneLogin/Ping/
  Google/Cognito, https only), scope normalization, JWT identity extraction
  (`preferred_username`/`upn`/`email`), and the `TokenType: EXTERNAL_IDP`
  header constants.
- Runtime executor (`open-sse/executors/kiro.ts`): send
  `TokenType: EXTERNAL_IDP` for external_idp accounts. CodeWhisperer only binds
  the org-IdP bearer to the Amazon Q Developer profile with this header;
  without it every call returns `ValidationException: Invalid ARN <clientId>`.
- Runtime + import token refresh (`open-sse/services/tokenRefresh.ts`,
  `src/lib/oauth/services/kiro.ts`): refresh external_idp tokens with a
  form-encoded public-client `refresh_token` grant against the org IdP's
  `tokenEndpoint` instead of AWS OIDC / the Kiro social endpoint.
- Quota (`open-sse/services/usage/kiro.ts`): send the same header on
  `GetUsageLimits` so organization quota resolves.
- Import routes: `POST /api/oauth/kiro/import` gains an external_idp branch
  (skips the `aorAAAAAG` gate, refreshes via the org IdP, stores
  clientId/tokenEndpoint/scope/region/profileArn); `GET /auto-import` now
  recognizes external_idp tokens in `~/.aws/sso/cache`, reads the profile ARN
  from the Kiro IDE `profile.json` (org tokens can't enumerate it via
  `ListAvailableProfiles`), and persists the connection. The profile.json
  reader is factored into a shared `readKiroIdeProfileArn()` helper.
- Validation schema (`kiroImportSchema`): accept `tokenEndpoint` + `scopes`.

Tests: new `tests/unit/kiro-external-idp.test.ts` (endpoint allowlist, scope
normalization, identity extraction, public-client refresh body, the org IdP
refresh path, and the `TokenType: EXTERNAL_IDP` header gating). Also hardens
`kiro-windows-auto-import-3363.test.ts` to isolate `USERPROFILE` (Windows
`os.homedir()` reads it, not `HOME`) so the probe never reads a real on-host
Kiro login.

* fix(changelog): restore #6363 bullet after release resync (CHANGELOG-eat guard)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(changelog): re-restore #6363 bullet after release sync

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(merge): restore #6126 clinepass files reverted by release auto-resolve + rebaseline own tokenRefresh growth

The release sync's merge auto-resolve silently reverted sibling PR #6126's
clinepass work (registry entry, catalog, oauth constants, clineAuth.ts, the
clinepass token-refresh case, and its tests) — all outside this PR's Kiro
external-IdP scope. Restored every affected file to the release version; the
remaining diff is Kiro-IdP-only. Rebaselined tokenRefresh.ts 2182->2249 (+67,
this PR's own external_idp refresh branch) with justification, and restored
the #6126 CHANGELOG bullet (re-inserting only this PR's own).

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: artickc <artickc@users.noreply.github.com>
2026-07-09 21:39:09 -03:00
Imam Wahyu Widodo
d3331f8bca feat(providers): ClinePass OAuth login — dual-auth on top of #5942 (reconciles #5924) (#6126)
* feat(providers): rebase ClinePass dual-auth (OAuth + BYOK) onto release/v3.8.47

ClinePass now offers both sign-in methods on its dashboard page: OAuth
(reusing the Cline WorkOS flow, primary "Connect" button) or a pasted
BYOK API key ("Manual API key"), instead of only the API-key-only
provider shipped in #5942.

- Registry: authType oauth + oauth urls, alias aligned to "cp" (matches
  the OAUTH_PROVIDERS catalog alias so <alias>/<modelId> routing
  resolves); keeps the #6165 forceStream:true fix (streaming-only API).
- Executor: new buildClinepassHeaders() (src/shared/utils/clineAuth.ts)
  picks buildClineHeaders() for an OAuth accessToken or a plain Bearer +
  Cline identification headers for a BYOK key — extracted to a leaf
  module to avoid growing the frozen open-sse/executors/default.ts.
- Refresh: dispatch clinepass to the shared refreshClineToken() (was
  falling through to the generic refresh and failing silently).
- Catalog: admit the BYOK path through a dedicated
  DUAL_AUTH_APIKEY_PROVIDER_IDS gate (src/lib/providers/catalog.ts) so
  POST /api/providers accepts an apikey connection without flipping
  isOAuth off (which would break the primary Connect->OAuth routing).
- Dashboard: render both "Connect" + "Manual API key" buttons for
  clinepass (ConnectionsHeaderToolbar.tsx, EmptyConnectionsPlaceholder.tsx).
- Dedup: removed the now-redundant API-key-only APIKEY_PROVIDERS_GATEWAYS
  entry so ClinePass is listed once (OAuth-primary).
- oauth.ts: added the clinepass catalog entry (was reverted by staleness
  during rebase); src/lib/oauth/providers/index.ts: clinepass -> cline.

This branch was ~167 commits / weeks behind release/v3.8.47; a real
merge surfaced 61 conflicting files, several of which are already-shipped
fixes (forceStream #6165, zed-hosted, requesty, agentrouter CC-wire-image,
NVIDIA/Mistral/kimi executor fixes, chatCore hardening) that a naive
resolution would have silently reverted. Reconstructed clean on top of
current release/v3.8.47, isolating and re-applying only the clinepass
dual-auth feature and preserving every already-shipped fix untouched.

tokenRefresh.ts's frozen-file cap raised by the irreducible 1-line
`case "clinepass":` switch label (config/quality/file-size-baseline.json,
justified inline); open-sse/executors/default.ts stays under its cap via
the buildClinepassHeaders() extraction.

Regression guard: tests/unit/clinepass-provider.test.ts (15/15, extended
with the dual-auth admission-gate and alias-consistency guards).

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* test(providers): update APIKEY_PROVIDERS spread-merge count 171->170

The ClinePass dual-auth rebase (this PR) removed the now-redundant
API-key-only APIKEY_PROVIDERS_GATEWAYS.clinepass entry (dedup — clinepass
is OAuth-primary now, with its BYOK path admitted through the
DUAL_AUTH_APIKEY_PROVIDER_IDS gate instead of a second catalog entry),
which drops the total APIKEY_PROVIDERS spread-merge count by one.
tests/unit/providers-constants-split.test.ts hardcoded the prior count
(171); updated to 170 to match, confirmed via CI (Unit Tests fast-path
1/2 and 2/2 both failed on the stale count).

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(oauth): register clinepass in PROVIDERS enum to fix Unknown provider error

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* chore(test): rebaseline oauth-providers-config.test.ts frozen size for clinepass entries

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(changelog): re-restore #6126 bullet after release sync

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: hajilok <hajilok@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
2026-07-09 20:37:40 -03:00
janeza2
9906dfc1ba fix(providers): update web model discovery (#6308)
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
2026-07-09 19:54:05 -03:00
Moseyuh333
b9d18dd8c4 Continue fix bugs and upgrade skill_collector (#6294)
* fix(skills): gate skill-collector CLI detection behind management auth + loopback

PR #6294 fork-main bundled genuinely new skill-collector CLI-detection routes
(GET /api/skills/collect/detect, POST /api/skills/collect/install) on top of
content already shipped via #6186. This reconstructs the PR against the
current release tip, keeping only the new detect/install routes and their
SKILL.md, and drops the 3 already-merged commits so two post-merge quality
fixes on /api/github-skills (Zod validation + sanitizeErrorMessage) are not
reverted.

- GET /api/skills/collect/detect spawned a child process per CLI_TOOL_IDS
  entry via getCliRuntimeStatus(), unauthenticated and reachable over any
  tunnel. All 3 routes (github-skills GET/POST, skills/collect/detect,
  skills/collect/install) now require requireManagementAuth(), matching
  every sibling /api/skills/* route.
- Classified /api/skills/collect/ in LOCAL_ONLY_API_PREFIXES and
  SPAWN_CAPABLE_PREFIXES (routeGuard.ts / spawnCapablePrefixes.ts) and added
  src/app/api/skills/collect to SPAWN_CAPABLE_ROUTE_ROOTS in
  check-route-guard-membership.ts so the automated gate actually scans it
  (Hard Rules #15 + #17).
- omniroute_github_skills_install MCP tool now reports the honest
  action: "planned" instead of "installed", matching the REST route.
- Dropped docker-compose.drive-d.yml, start.sh, and the unrelated
  @types/node/settings.ts changes (personal dev-machine / out-of-scope).
- Added route-level tests for all 3 routes + the 3 MCP tools (auth-required
  and no-stack-trace-leak assertions) and a route-guard regression test.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(quality): register new routeGuard covering test in stryker.conf.json

check:mutation-test-coverage --strict (Fast Quality Gates) flagged
tests/unit/authz/route-guard-skills-collect.test.ts as a covering unit test
for src/server/authz/routeGuard.ts that was missing from tap.testFiles, so
its mutant kills would silently not count toward the mutation-test baseline.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* chore: resync CHANGELOG after merging release/v3.8.47

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: Moseyuh333 <Moseyuh333@users.noreply.github.com>
2026-07-09 19:43:43 -03:00
nowhats-br
2a5a819dbe fix: update Dockerfile with --allow-scripts for better-sqlite3 compil… (#6700)
* fix(docker): compile better-sqlite3 via direct node-gyp rebuild in the Dockerfile

The `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate
supply-chain hardening) and then re-enables the native build for the one package
that needs it. `npm rebuild better-sqlite3` re-runs that indirectly through the
package's own install script, which under npm 11 depends on npm's script-allowlist
machinery correctly re-enabling it — some self-hosted build environments (e.g.
Dokploy) hit a broken/mismatched native binding through that indirection.

Invoke `node-gyp rebuild` directly inside `node_modules/better-sqlite3` instead,
bypassing npm's script-running layer entirely, so the compile step is deterministic
regardless of npm version or ignore-scripts allowlist behavior.

Rebased onto the current release/v3.8.47 tip: dropped this branch's stale
electron/package.json + package-lock.json diff (would have reverted the
electron 42->43 ABI-148 fix from #6605) and the unconsumed root `allowScripts`
package.json field (npm does not read that key; has zero effect).

Regression guard: tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(changelog): restore CHANGELOG bullets eaten by release sync

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(changelog): re-restore CHANGELOG bullet after further release sync

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(changelog): re-restore CHANGELOG bullet after further release sync

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(changelog): re-restore CHANGELOG bullet after further release sync

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(changelog): correct CHANGELOG restoration (previous attempt had a script-path bug)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(changelog): re-restore #6700 bullet after #6496 release sync

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(changelog): re-restore CHANGELOG bullet after further release sync

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: nowhats-br <nowhats-br@users.noreply.github.com>
2026-07-09 18:57:06 -03:00
SeaXen
889fffddbe fix(cloudflare-relay): use Service Worker syntax with body_part metadata (#6416) (#6496)
* fix(providers): Cloudflare relay Worker uses Service Worker syntax + body_part

CONTEXT: #6416/#6618 fixed the multipart Content-Type but the emitted
worker source still used ES-module syntax (`export default { fetch }`)
with `main_module` metadata. Cloudflare's Workers upload API parses a
plain `application/javascript` script part as Service Worker syntax
regardless of `main_module`, and `main_module` requires the script to
actually be an ES module — so the upload was still rejected.

CHANGE: buildCloudflareWorkerScript() now emits Service Worker syntax
(`addEventListener("fetch", ...)`, no top-level `export`) and the
upload metadata uses `body_part` instead of `main_module`.

Also restores the SSRF-guard bracket-stripping regex for bracketed
IPv6 hosts (`[::1]`, `[fd00::1]`) that an earlier revision of this
change accidentally double-escaped, with regression coverage added to
tests/unit/relay-deploy-5128.test.ts. Updates the sibling
tests/unit/proxy-pool-cloudflare-workers-deployer.test.ts assertion
that still expected the old ES-module contract.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(changelog): restore CHANGELOG bullets eaten by release sync

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(changelog): re-restore CHANGELOG bullet after further release sync

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(changelog): re-restore CHANGELOG bullet after further release sync

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(changelog): re-restore CHANGELOG bullet after further release sync

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(changelog): correct CHANGELOG restoration (previous attempt had a script-path bug)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: SeaXen <SeaXen@users.noreply.github.com>
2026-07-09 18:43:19 -03:00
SeaXen
6557f44bd2 feat(settings): 9router-style Routing Strategy card + sticky parity (#6678)
* feat(dashboard): 9router-parity Routing Strategy card + provider/combo sticky override (#6678)

Add a Routing Strategy settings card (Settings -> Routing) surfacing account
round-robin/sticky-limit knobs plus a new combo-level sticky round-robin
(comboStickyRoundRobinLimit), and a per-provider account-routing override
(providerStrategies) wired into getProviderCredentials() ahead of the global
fallback strategy. Rebased onto release/v3.8.47 (credit-preserving
reconstruction: unrelated package.json/electron/proxyDispatcher drift from the
PR's stale base was dropped, only the author's own 12 files were re-applied).
Split ProviderAccountRoutingCard/RoutingStrategyCard into smaller
hook+subcomponent pieces to stay under the frozen complexity/file-size gates;
rebaselined ProviderDetailPageClient.tsx/auth.ts's frozen file-size caps for
the small additive growth.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* chore(quality): register combo-rr-sticky-9router.test.ts in stryker tap.testFiles (#6678)

CI's Fast Quality Gates -> check:mutation-test-coverage --strict flagged the new
test as missing from stryker.conf.json's tap.testFiles (it covers the mutated
module open-sse/services/combo/rrState.ts). Adds the single entry, alphabetized
next to the existing combo-rr-fallback-advance-948.test.ts.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(changelog): restore CHANGELOG bullets eaten by release sync

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(changelog): re-restore CHANGELOG bullet after further release sync

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(changelog): re-restore CHANGELOG bullet after further release sync

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: SeaXen <SeaXen@users.noreply.github.com>
2026-07-09 18:22:43 -03:00
Arthur Bodera
d76aa40ee6 fix(chatgpt-web): render citations as markdown links (#6635)
* fix(providers): render ChatGPT-web citation markers as Markdown links

ChatGPT Web responses leaked raw chatgpt.com UI citation markup (private-use
marker tokens like `citeturn0search0`, `entity[...]`) instead of real
Markdown links, since these are normally resolved client-side by chatgpt.com's
own JS using `message.metadata.content_references`.

cleanChatGptText() now resolves content_references (grouped webpages, footnote
sources, inline webpage/url mentions) into `[label](url)` Markdown links for
the streaming and non-streaming response builders and the GPT-5.5 Pro
stream_handoff polled-answer path, falling back to stripping any marker with
no resolvable source.

The citation parsing/rendering logic was extracted into a new pure sibling
module (open-sse/executors/chatgpt-web/citations.ts), decomposed into small
per-reference-type helpers, to keep the executor under the frozen file-size
cap and the complexity/cognitive-complexity ratchets. Regression tests moved
to a dedicated tests/unit/chatgpt-web-citations.test.ts for the same reason.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(changelog): restore CHANGELOG bullets eaten by release sync

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(changelog): re-restore CHANGELOG bullet after further release sync

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(changelog): re-restore CHANGELOG bullet after further release sync

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: Thinkscape <thinkscape@users.noreply.github.com>
2026-07-09 17:37:20 -03:00
Chirag Singhal
237bd59173 fix(api): sanitize catch-block error.message in middleware/hooks routes (#6645)
* fix(api): sanitize catch-block error.message in middleware/hooks routes

POST /api/middleware/hooks and PUT /api/middleware/hooks/[name] returned
the raw error?.message in their 500 response bodies (Hard Rule #12),
which could leak internal SQLite error text/paths on a DB failure. Both
now route through sanitizeErrorMessage() from open-sse/utils/error.ts,
matching the pattern already used elsewhere in the codebase.

Regression guard: tests/unit/middleware-hooks-error-sanitization.test.ts

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* test(mutation): register middleware-hooks-error-sanitization in stryker tap.testFiles

The mutation test-coverage gate (check:mutation-test-coverage --strict)
flagged tests/unit/middleware-hooks-error-sanitization.test.ts as
covering open-sse/utils/error.ts but missing from stryker.conf.json's
tap.testFiles list.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(changelog): restore CHANGELOG bullets eaten by release sync

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(changelog): re-restore CHANGELOG bullet after further release sync

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
2026-07-09 17:31:52 -03:00
Hernan Javier Ardila Sanchez
912ff8d1c4 fix(vision-bridge): auto-reroute non-vision models to fastest vision model when images detected (#6640)
* fix(electron): bump electron 42→43 + build better-sqlite3 from source (ABI 148) (#6605)

fix(electron): bump electron 42→43 + rebuild better-sqlite3 from source against the Electron ABI (148).

Electron 43 raises NODE_MODULE_VERSION to 148; better-sqlite3@12.11.1 has no electron-v148 prebuild, so the packaged app died with 'Nenhum driver SQLite disponível'. prepare-electron-standalone now compiles better-sqlite3 from source against the electron headers into build/Release (where 'bindings' resolves it). Validated by Electron Package Smoke (green) + local (node_register_module_v148).

Supersedes #6378. (--admin: the only reds are SonarQube/SonarCloud failing on a coverage-report artifact digest-mismatch — a GitHub Actions infra flake, not this diff; Sonar is green on main and the diff touches only the electron build.)

* deps: bump the development group across 1 directory with 6 updates (#6588)

deps: bump the development group (6 updates). Rebased onto current main; all checks green after the electron-smoke fix (#6605).

* fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump (#6620)

fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump.

undici 8.6+ changed ProxyAgent to forward plain-HTTP via request-proxy instead of CONNECT, breaking OAuth refresh through a connection proxy (501). proxyDispatcher now passes proxyTunnel:true. Validated: Unit Tests 3/8 (the OAuth-proxy test) green, new regression test green (fails without the fix on undici 8.7), SonarQube green.

Supersedes #6380. (--admin: the only red is Electron Package Smoke failing on a next-build artifact 'digest-mismatch' — a GitHub Actions infra flake corrupting the asar ('file data stream has unexpected number of bytes'); the better-sqlite3 rebuild itself succeeded (gyp ok) and the electron path is unchanged from #6605 which passed the smoke. Not this diff.)

* fix(vision-bridge): auto-reroute non-vision models to fastest vision model when images detected

The VisionBridgeGuardrail was describing images as text via a vision model
and sending text to the original (non-vision) model. This defeated the purpose
when the final target was already vision-capable (auto/vision, combos with
vision targets) and never actually rerouted requests to a vision model.

Changes:
- Individual non-vision models + images → reroute  to the fastest
  available vision-capable model (via getBestVisionModel), keeping images intact
- Auto/ prefix models (auto/vision, auto) → skip guardrail entirely, letting
  the auto-combo resolver handle vision-capable model selection
- Combo mappings with non-vision targets → keep existing describe behavior
  (fallback path via checkModelHasComboMapping)
- chat.ts: sync modelStr from body.model after guardrail execution so downstream
  routing uses the rerouted model

* fix(vision-bridge): use getBestVisionModel auto-routing instead of fixed model

Address Gemini review feedback: getBestVisionConfig({}) with empty object
bypassed auto-routing by always defaulting to a fixed model. Auto-select
the best vision model from available providers instead.

* fix: compact modelStr sync to stay under file-size cap (1632)

* fix: remove debug log, orphaned brace to keep file under cap

* chore: trigger CI re-run with file-size fix and PR evidence

* chore: rebaseline chat.ts frozen cap to 1754 (PR #6640 +3 lines)

* fix(auto-combo): respect hidden models from dashboard toggle

getHiddenModelsByProvider() only queried modelCompatOverrides and
customModels namespaces, missing the hiddenModels namespace used by
the dashboard hide/unhide toggle. Auto-combo candidates now filter
out models the user explicitly hid.

* fix(changelog): restore CHANGELOG bullets eaten by release sync

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: herjarsa <herjarsa@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
2026-07-09 17:15:53 -03:00
Diego Rodrigues de Sa e Souza
5e5447a2ba fix(sse): count gate/combo-rejected requests in per-api-key usage (#6698)
Requests rejected before handleChatCore — a pipeline-gate rejection
(provider circuit breaker OPEN / model cooldown) or a combo whose
targets were all exhausted — short-circuited in chat.ts and only wrote
a call_logs row (dashboard/logs). They never reached persistFailureUsage,
so no usage_history row was created and the per-api-key usage counter
(getApiKeyUsageRows reads usage_history) never incremented. An API key
whose traffic was entirely gate/breaker-rejected showed zero requests
despite real usage.

Route both rejection paths through recordRejectedRequestUsage(), which
writes the call_logs row (unchanged visibility) AND a usage_history row
attributed to the api key with success:false, mirroring persistFailureUsage.

Regression guard: tests/unit/rejected-request-usage.test.ts.
2026-07-09 16:50:29 -03:00
MikeTuev
0d20205f92 fix(sse): preserve server-tool literal names in message history and tool_choice (#6586)
* fix(sse): preserve server-tool literal names in message history and tool_choice

The v3.8.36 guard (isAnthropicServerToolType, #2943) protects Anthropic
server tools (web_search_20250305, bash_20250124, ...) from the tool-name
cloak only in the tools[] array. The same reserved literal names were still
rewritten in message-history tool_use blocks and in tool_choice, and
remapToolNamesInRequest had no guard at all (bash -> Bash).

The resulting asymmetry — tools[] keeps 'web_search' while the history
reference becomes 'WebSearch' — makes Anthropic reject every follow-up turn
of a native web-search conversation:

  [400] Tool 'WebSearch' not found in provided tools

Collect the declared server-tool names once per request and skip them in
every rewrite path of both remapToolNamesInRequest and
cloakThirdPartyToolNames (tools[], message history, tool_choice). Plain
custom tools with the same names (no server type) remain remapped/cloaked
exactly as before, symmetrically in all sections.

Surfaced on Claude Code 2.1.x native WebSearch; same class as CLIProxyAPI
#1094/#1179. TDD: 5 failing repro tests -> guard -> 7/7 green (92/92 across
the remapper suite), typecheck:core clean.

* fix(sse): skip null entries in tools[] before server-tool type check

Review follow-up (gemini-code-assist): a null element in tools[] made the
new isAnthropicServerToolType(tool.type) check throw. The crash path is
pre-existing (String(tool.name) on the next line threw identically), but
the guard is cheap and mirrors the null checks already used in
cloakThirdPartyToolNames. Adds a regression test (8/8 green).
2026-07-09 16:49:59 -03:00
Xiangzhe
edae0cf33f Expose per-combo reasoning token buffer toggle (#6702)
* fix(combos): default reasoning token buffer off

* feat(combos): expose reasoning token buffer toggle

* fix(combos): keep reasoning-token buffer default enabled, opt-out toggle

#6702 shipped bundled with #6536's own commit (identical SHA
37ac38f17f), flipping the combo-wide reasoningTokenBufferEnabled
default from true to false. #6536 was subsequently closed by the
author in favor of #6714, which explicitly keeps the existing
default-enabled buffer behavior and instead clamps the buffer to the
model's known output cap. Reconciled #6702 with that resolution:
dropped the default-flip changes across comboConfig.ts, combo.ts,
comboSetup.ts, ComboDefaultsTab.tsx, and the combo-defaults settings
route (plus their test assertions), and inverted the new per-combo
ReasoningTokenBufferToggle to opt-out semantics (`!== false`) so an
existing combo's behavior is unchanged unless the operator explicitly
unchecks it.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-09 16:49:20 -03:00
KooshaPari
abfced8b28 feat(sandbox): native Apple Container, WSL, OrbStack, Podman runtime support (#6611)
* feat(sandbox): native Apple Container, WSL, OrbStack, Podman runtime support

* fix(skills): align sandbox fallback kill container-name convention

sandbox.ts's docker-fallback kill path (used only when cachedProvider is
unexpectedly null) still targeted the pre-PR omniroute-sandbox-${id}
container name, while containerProvider.ts's SANDBOX_NAME now produces
omniroute-${id}. Align the fallback naming so it matches the provider
convention, with a regression test covering kill()/killAll() before a
provider has ever been resolved.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(docs): document SKILLS_SANDBOX_RUNTIME and drop unrelated env leftovers

Two fixes surfaced by CI's env/docs contract gate:

- Add the SKILLS_SANDBOX_RUNTIME row to docs/reference/ENVIRONMENT.md so
  the new container-runtime override introduced by this PR is documented,
  matching .env.example.
- Remove the Substrate/Bifrost/OTEL .env.example blocks that leaked in
  from this branch's stale main-based history during the release-branch
  sync merge — none of that belongs to this PR (native container
  runtimes for the skill sandbox) and none of it exists on
  release/v3.8.47 yet.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-09 16:48:49 -03:00
enjoyer-hub
e697670046 fix(cli): detect WinGet Claude Code on Windows (#6647)
* fix(electron): bump electron 42→43 + build better-sqlite3 from source (ABI 148) (#6605)

fix(electron): bump electron 42→43 + rebuild better-sqlite3 from source against the Electron ABI (148).

Electron 43 raises NODE_MODULE_VERSION to 148; better-sqlite3@12.11.1 has no electron-v148 prebuild, so the packaged app died with 'Nenhum driver SQLite disponível'. prepare-electron-standalone now compiles better-sqlite3 from source against the electron headers into build/Release (where 'bindings' resolves it). Validated by Electron Package Smoke (green) + local (node_register_module_v148).

Supersedes #6378. (--admin: the only reds are SonarQube/SonarCloud failing on a coverage-report artifact digest-mismatch — a GitHub Actions infra flake, not this diff; Sonar is green on main and the diff touches only the electron build.)

* deps: bump the development group across 1 directory with 6 updates (#6588)

deps: bump the development group (6 updates). Rebased onto current main; all checks green after the electron-smoke fix (#6605).

* fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump (#6620)

fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump.

undici 8.6+ changed ProxyAgent to forward plain-HTTP via request-proxy instead of CONNECT, breaking OAuth refresh through a connection proxy (501). proxyDispatcher now passes proxyTunnel:true. Validated: Unit Tests 3/8 (the OAuth-proxy test) green, new regression test green (fails without the fix on undici 8.7), SonarQube green.

Supersedes #6380. (--admin: the only red is Electron Package Smoke failing on a next-build artifact 'digest-mismatch' — a GitHub Actions infra flake corrupting the asar ('file data stream has unexpected number of bytes'); the better-sqlite3 rebuild itself succeeded (gyp ok) and the electron path is unchanged from #6605 which passed the smoke. Not this diff.)

* fix(cli): detect WinGet Claude Code on Windows

* chore(merge): drop unrelated main-drift from PR fork (deps/electron/proxy files belong to #6620, not this PR)

Restores electron/package-lock.json, electron/package.json, package-lock.json,
package.json, open-sse/utils/proxyDispatcher.ts, scripts/build/prepare-electron-standalone.mjs
and tests/unit/proxy-dispatcher-family.test.ts to origin/release/v3.8.47's content.
The PR fork branched from a state of main that already includes #6620 (proxy CONNECT
tunnel fix + deps bump), which is not yet synced into release/v3.8.47 — the 3-way
merge would otherwise silently carry that unrelated content into this doc-only PR.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* chore(quality): rebaseline cliRuntime.ts file-size freeze for #6647 (1100->1110)

The file was already exactly at the frozen 1100-line cap on release/v3.8.47.
PR #6647's WinGet Claude Code detection path adds 10 lines (irreducible —
the 62-char package folder name forces Prettier's 100-char width to break
the path.join call across the same multi-line form used by every other
long path in this function), tripping the Fast Quality Gates check:file-size
job. Bumping the frozen cap to the file's real new size per the documented
allowlist-with-justification policy (this is a pass/fail policy gate, not
the ratchet metrics system).

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: quanturbo <faralechko@gmail.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
2026-07-09 16:46:41 -03:00
Chirag Singhal
2b33c6c635 docs(routing): reconcile 17 vs 18 public-strategy count in AUTO-COMBO (#6646)
* fix(electron): bump electron 42→43 + build better-sqlite3 from source (ABI 148) (#6605)

fix(electron): bump electron 42→43 + rebuild better-sqlite3 from source against the Electron ABI (148).

Electron 43 raises NODE_MODULE_VERSION to 148; better-sqlite3@12.11.1 has no electron-v148 prebuild, so the packaged app died with 'Nenhum driver SQLite disponível'. prepare-electron-standalone now compiles better-sqlite3 from source against the electron headers into build/Release (where 'bindings' resolves it). Validated by Electron Package Smoke (green) + local (node_register_module_v148).

Supersedes #6378. (--admin: the only reds are SonarQube/SonarCloud failing on a coverage-report artifact digest-mismatch — a GitHub Actions infra flake, not this diff; Sonar is green on main and the diff touches only the electron build.)

* deps: bump the development group across 1 directory with 6 updates (#6588)

deps: bump the development group (6 updates). Rebased onto current main; all checks green after the electron-smoke fix (#6605).

* fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump (#6620)

fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump.

undici 8.6+ changed ProxyAgent to forward plain-HTTP via request-proxy instead of CONNECT, breaking OAuth refresh through a connection proxy (501). proxyDispatcher now passes proxyTunnel:true. Validated: Unit Tests 3/8 (the OAuth-proxy test) green, new regression test green (fails without the fix on undici 8.7), SonarQube green.

Supersedes #6380. (--admin: the only red is Electron Package Smoke failing on a next-build artifact 'digest-mismatch' — a GitHub Actions infra flake corrupting the asar ('file data stream has unexpected number of bytes'); the better-sqlite3 rebuild itself succeeded (gyp ok) and the electron path is unchanged from #6605 which passed the smoke. Not this diff.)

* docs(routing): reconcile 17 vs 18 public-strategy count in AUTO-COMBO

* chore(merge): drop unrelated main-drift from PR fork (deps/electron/proxy files belong to #6620, not this PR)

Restores electron/package-lock.json, electron/package.json, package-lock.json,
package.json, open-sse/utils/proxyDispatcher.ts, scripts/build/prepare-electron-standalone.mjs
and tests/unit/proxy-dispatcher-family.test.ts to origin/release/v3.8.47's content.
The PR fork branched from a state of main that already includes #6620 (proxy CONNECT
tunnel fix + deps bump), which is not yet synced into release/v3.8.47 — the 3-way
merge would otherwise silently carry that unrelated content into this doc-only PR.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
2026-07-09 16:46:11 -03:00
Chirag Singhal
484abed8c1 docs: sync routing-strategy count to 18 across README + AGENTS.md (#6644)
* fix(electron): bump electron 42→43 + build better-sqlite3 from source (ABI 148) (#6605)

fix(electron): bump electron 42→43 + rebuild better-sqlite3 from source against the Electron ABI (148).

Electron 43 raises NODE_MODULE_VERSION to 148; better-sqlite3@12.11.1 has no electron-v148 prebuild, so the packaged app died with 'Nenhum driver SQLite disponível'. prepare-electron-standalone now compiles better-sqlite3 from source against the electron headers into build/Release (where 'bindings' resolves it). Validated by Electron Package Smoke (green) + local (node_register_module_v148).

Supersedes #6378. (--admin: the only reds are SonarQube/SonarCloud failing on a coverage-report artifact digest-mismatch — a GitHub Actions infra flake, not this diff; Sonar is green on main and the diff touches only the electron build.)

* deps: bump the development group across 1 directory with 6 updates (#6588)

deps: bump the development group (6 updates). Rebased onto current main; all checks green after the electron-smoke fix (#6605).

* fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump (#6620)

fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump.

undici 8.6+ changed ProxyAgent to forward plain-HTTP via request-proxy instead of CONNECT, breaking OAuth refresh through a connection proxy (501). proxyDispatcher now passes proxyTunnel:true. Validated: Unit Tests 3/8 (the OAuth-proxy test) green, new regression test green (fails without the fix on undici 8.7), SonarQube green.

Supersedes #6380. (--admin: the only red is Electron Package Smoke failing on a next-build artifact 'digest-mismatch' — a GitHub Actions infra flake corrupting the asar ('file data stream has unexpected number of bytes'); the better-sqlite3 rebuild itself succeeded (gyp ok) and the electron path is unchanged from #6605 which passed the smoke. Not this diff.)

* docs: sync routing-strategy count to 18 across README + AGENTS.md

* chore(merge): drop unrelated main-drift from PR fork (deps/electron/proxy files belong to #6620, not this PR)

Restores electron/package-lock.json, electron/package.json, package-lock.json,
package.json, open-sse/utils/proxyDispatcher.ts, scripts/build/prepare-electron-standalone.mjs
and tests/unit/proxy-dispatcher-family.test.ts to origin/release/v3.8.47's content.
The PR fork branched from a state of main that already includes #6620 (proxy CONNECT
tunnel fix + deps bump), which is not yet synced into release/v3.8.47 — the 3-way
merge would otherwise silently carry that unrelated content into this doc-only PR.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
2026-07-09 16:45:42 -03:00
Chirag Singhal
6e16ae7b32 docs(claude): fix p2c casing to match ROUTING_STRATEGY_VALUES (#6643)
* fix(electron): bump electron 42→43 + build better-sqlite3 from source (ABI 148) (#6605)

fix(electron): bump electron 42→43 + rebuild better-sqlite3 from source against the Electron ABI (148).

Electron 43 raises NODE_MODULE_VERSION to 148; better-sqlite3@12.11.1 has no electron-v148 prebuild, so the packaged app died with 'Nenhum driver SQLite disponível'. prepare-electron-standalone now compiles better-sqlite3 from source against the electron headers into build/Release (where 'bindings' resolves it). Validated by Electron Package Smoke (green) + local (node_register_module_v148).

Supersedes #6378. (--admin: the only reds are SonarQube/SonarCloud failing on a coverage-report artifact digest-mismatch — a GitHub Actions infra flake, not this diff; Sonar is green on main and the diff touches only the electron build.)

* deps: bump the development group across 1 directory with 6 updates (#6588)

deps: bump the development group (6 updates). Rebased onto current main; all checks green after the electron-smoke fix (#6605).

* fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump (#6620)

fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump.

undici 8.6+ changed ProxyAgent to forward plain-HTTP via request-proxy instead of CONNECT, breaking OAuth refresh through a connection proxy (501). proxyDispatcher now passes proxyTunnel:true. Validated: Unit Tests 3/8 (the OAuth-proxy test) green, new regression test green (fails without the fix on undici 8.7), SonarQube green.

Supersedes #6380. (--admin: the only red is Electron Package Smoke failing on a next-build artifact 'digest-mismatch' — a GitHub Actions infra flake corrupting the asar ('file data stream has unexpected number of bytes'); the better-sqlite3 rebuild itself succeeded (gyp ok) and the electron path is unchanged from #6605 which passed the smoke. Not this diff.)

* docs(claude): fix p2c casing to match ROUTING_STRATEGY_VALUES

* chore(merge): drop unrelated main-drift from PR fork (deps/electron/proxy files belong to #6620, not this PR)

Restores electron/package-lock.json, electron/package.json, package-lock.json,
package.json, open-sse/utils/proxyDispatcher.ts, scripts/build/prepare-electron-standalone.mjs
and tests/unit/proxy-dispatcher-family.test.ts to origin/release/v3.8.47's content.
The PR fork branched from a state of main that already includes #6620 (proxy CONNECT
tunnel fix + deps bump), which is not yet synced into release/v3.8.47 — the 3-way
merge would otherwise silently carry that unrelated content into this doc-only PR.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
2026-07-09 16:45:11 -03:00
Paijo
1291e9bf26 fix(providers): web-cookie fallback validation reports unsupported instead of a false valid (#6309)
validateWebCookieProvider() previously required a providerRegistry.ts entry and
returned "Provider not found in registry" for web-cookie-only providers like
lmarena, gemini-business, poe-web, venice-web and v0-vercel-web. A fallback to
WEB_COOKIE_PROVIDERS[provider].website was proposed, but live verification showed
probing `${website}/models` does not reliably signal session validity for these
(redirects/SPA 200s regardless of cookie validity) — it would report an expired
or garbage cookie as valid, which is worse than an honest "not supported". Until
each provider has a verified, side-effect-free auth probe against its real API
host, the fallback now returns `unsupported: true` with no network call. Also
reverts the probe transport from validationRead back to directHttpsRequest,
which fixes a globalThis.fetch mock/patch-timing mismatch that made the
pre-existing tests/unit/provider-validation-web-cookie-auth007.test.ts hit the
live network in CI, and adds the missing Cookie header to the probe request.

Regression guard: tests/unit/web-cookie-validation-fallback.test.ts.

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-09 16:45:01 -03:00
Hamsa_M
aba98fd227 fix(cli): per-agent DNS, startup guards, and batched Windows hosts writes (#6338)
DNS toggle in AgentBridge was broken for 8 of 9 agents: addDNSEntry/
removeDNSEntry always resolved the legacy Antigravity default hosts
regardless of which agent's dns_enabled flag was flipped. Both now
accept an optional agentId and resolve hosts via ALL_TARGETS; the
[id]/dns route passes id through and returns 404 for an unknown agent
instead of silently falling back to the defaults.

startMitmInternal() now wraps generateCert(), the provisionDnsEntries()
call, and the PID-file write in try/catch so a mid-startup failure
can't orphan the already-spawned MITM child process.

On Windows, addDNSEntries/removeDNSEntries batch every missing/present
entry into a single elevated PowerShell invocation instead of one UAC
prompt per host line.

Scope note: this PR originally bundled an unrelated SkillOpt feature
(DB migration, 6 API routes, dashboard UI) and a checks-free CI build
workflow alongside this DNS/startup fix. Both were dropped here as
out-of-scope per review-group-prs analysis (2-implementing plan);
only the DNS/startup-guard delta (dnsConfig.ts, manager.ts, the [id]/dns
route, and their tests) is applied.

Co-authored-by: hamsa0x7 <hamsa0x7@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-09 16:44:50 -03:00
Septianata Rizky Pratama
606d1cbbd3 fix: move tier-flow SVG images to public directory (#6538)
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
2026-07-09 16:44:39 -03:00
backryun
0e4145d257 fix(providers): remove obsolete providers (glhf, kluster, cablyai, inclusionai) (#6675)
Drop dead catalog/registry entries, keep Synthetic as the GLHF replacement path,
regenerate provider reference/docs counts, and lock APIKEY family-split + file-size
gates so CI stays green.

Ignore prettier on freeModelCatalog.data.ts so dense one-line budget rows are not
expanded past the 800-line new-file cap.
2026-07-09 16:44:28 -03:00
Diego Rodrigues de Sa e Souza
f4fb0d310b docs(readme): update star badges and star history chart links 2026-07-09 01:33:33 -03:00
Diego Rodrigues de Sa e Souza
902e66805c chore(vscode): update search exclude patterns and add documentation
Add several directories to the search exclude list to improve search
performance and add a comment explaining why certain directories are
not being hidden from the file explorer.
2026-07-08 23:30:38 -03:00
Diego Rodrigues de Sa e Souza
f4643a2476 docs(changelog): add v3.8.47 Contributors section (32 contributors) 2026-07-08 22:33:35 -03:00
Diego Rodrigues de Sa e Souza
5144712ab6 ci(vps): honor VPS_ALWAYS_ON — release teardown is a no-op on the dedicated 24/7 host (#6693)
The .113 VM is now a dedicated, always-on CI host so day-to-day quality.yml PRs
(PR→release/**) use the 32-core VPS, not just release CI. release-runner-down.sh must not
flip USE_VPS_RUNNER=false / shut the VM down when VPS_ALWAYS_ON=true, or every PR after a
release would fall back to ubuntu-latest. Legacy on-demand teardown still applies when the
var is unset/false.
2026-07-08 18:37:04 -03:00
Diego Rodrigues de Sa e Souza
632d304939 ci(quality): route the 3 heavy fast-path jobs to the self-hosted VPS pool when USE_VPS_RUNNER is on (#6691)
Extends the same dynamic-runner gate ci.yml already uses (build/test-unit/test-vitest)
to quality.yml's fast-gates/fast-vitest/fast-unit — the ~9min-on-ubuntu jobs that run on
every PR→release/**. Inert until USE_VPS_RUNNER flips to true (falls back to ubuntu-latest
when the var is unset/false OR the PR is a fork — own-origin branches only, never the LAN
runner for fork code). lint-guard/merge-integrity stay on ubuntu-latest (trivial; keeps VPS
concurrency low). No behavior change today.
2026-07-08 17:45:32 -03:00
Diego Rodrigues de Sa e Souza
a6ea6074cb fix(cli): compression REST fallback uses canonical defaultMode + JSON object cells (#6571) (#6682)
* fix(cli): compression REST fallback uses canonical defaultMode + JSON object cells (#6571)

* test(cli): align existing compression-command tests to the #6571 canonical field contract (engine→strategy/defaultMode)
2026-07-08 16:50:11 -03:00
ThongAccount
1188847f0f feat: add setting for provider/model-specific parameters (#6649)
* fix(electron): bump electron 42→43 + build better-sqlite3 from source (ABI 148) (#6605)

fix(electron): bump electron 42→43 + rebuild better-sqlite3 from source against the Electron ABI (148).

Electron 43 raises NODE_MODULE_VERSION to 148; better-sqlite3@12.11.1 has no electron-v148 prebuild, so the packaged app died with 'Nenhum driver SQLite disponível'. prepare-electron-standalone now compiles better-sqlite3 from source against the electron headers into build/Release (where 'bindings' resolves it). Validated by Electron Package Smoke (green) + local (node_register_module_v148).

Supersedes #6378. (--admin: the only reds are SonarQube/SonarCloud failing on a coverage-report artifact digest-mismatch — a GitHub Actions infra flake, not this diff; Sonar is green on main and the diff touches only the electron build.)

* deps: bump the development group across 1 directory with 6 updates (#6588)

deps: bump the development group (6 updates). Rebased onto current main; all checks green after the electron-smoke fix (#6605).

* fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump (#6620)

fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump.

undici 8.6+ changed ProxyAgent to forward plain-HTTP via request-proxy instead of CONNECT, breaking OAuth refresh through a connection proxy (501). proxyDispatcher now passes proxyTunnel:true. Validated: Unit Tests 3/8 (the OAuth-proxy test) green, new regression test green (fails without the fix on undici 8.7), SonarQube green.

Supersedes #6380. (--admin: the only red is Electron Package Smoke failing on a next-build artifact 'digest-mismatch' — a GitHub Actions infra flake corrupting the asar ('file data stream has unexpected number of bytes'); the better-sqlite3 rebuild itself succeeded (gyp ok) and the electron path is unchanged from #6605 which passed the smoke. Not this diff.)

* feat(db): add provider param filter config store (key_value namespace)

Add paramFilters.ts module for CRUD against provider_param_filters
namespace in the key_value table, with in-memory cache + generation
counter invalidation. Supports denylist/allowlist per provider and
per model, plus auto-learn flag.

Migration 118 documents the namespace (no schema change).

Issue: #6625

* feat(proxy): add detectUnsupportedParam regex for auto-learning

Add UNSUPPORTED_PARAM_RE and detectUnsupportedParam() to extract
the offending parameter name from upstream 400 error messages like
'Unsupported parameter(s): thinking'.

Issue: #6625

* feat(proxy): extend stripUnsupportedParams with config-driven denylist/allowlist

Add applyConfigFilters() called after hardcoded STRIP_RULES in
stripUnsupportedParams(). Config-driven rules (DB-backed via
paramFilters.ts) support provider-level and model-level:
  1. Provider denylist (delete body[key])
  2. Model denylist (delete body[key])
  3. Provider allowlist (restore from pre-strip snapshot)
  4. Model allowlist (restore from pre-strip snapshot)

Allowlist only restores keys the client actually sent — never
introduces new params.

Issue: #6625

* feat(proxy): wire auto-learn of unsupported params into 400-downgrade loop

When a provider returns 400 with 'Unsupported parameter: X' and the
provider config has autoLearn enabled, auto-detect the param name
via detectUnsupportedParam(), persist it to the provider's block
list via addParamToBlocklist(), then strip and retry.

Issue: #6625

* test: add tests for provider param filter denylist/allowlist/auto-learn

Three new test files:
- param-filters-apply.test.ts — hardcoded rules regression + direct
  applyConfigFilters tests (no DB dependency)
- param-filters-db.test.ts — CRUD against key_value, cache invalidation,
  full filter pipeline (DB-backed config → stripUnsupportedParams),
  16 tests in isolated temp DB
- param-filters-auto-learn.test.ts — UNSUPPORTED_PARAM_RE regex matching
  and detectUnsupportedParam edge cases

All existing tests unchanged and passing.

Issue: #6625

* feat(proxy): add global auto-learn flag for unsupported params

Add isAutoLearnGloballyEnabled() and setGlobalAutoLearnEnabled() to
paramFilters.ts. The global flag (stored as key __global__ in the
provider_param_filters namespace) acts as a master switch: when
enabled, ALL providers auto-learn unsupported params from 400 errors.

In base.ts, the auto-learn check now evaluates:
  shouldAutoLearn = isAutoLearnGloballyEnabled() || perProviderConfig?.autoLearn

Global flag defaults to false (opt-in). Tests cover enable/disable/
default/no-interference-with-per-provider-config.

Issue: #6625

* fix: apply PR#6649 review feedback — model-scoped auto-learn and precedence order

Fixes from gemini-code-assist[bot] review:
- HIGH: Auto-learn now scoped to the specific model that triggered the 400
  (addParamToBlocklist(this.provider, autoLearned, model)) instead of
  adding to the provider-level blocklist globally
- HIGH: Reordered applyConfigFilters so model-level operations run AFTER
  provider-level operations (model denylist → model allowlist override
  provider allowlist → provider denylist)
- MEDIUM: Include model name in auto-learn log message

Adds regression test verifying model-level denylist beats provider-level
allowlist.

Issue: #6625
PR: #6649

* feat(ui): add provider-level param filter section to detail page

Add ProviderParamFilterSection component rendered on each provider
detail page, backed by GET|PUT|DELETE /api/providers/[id]/param-filters.

UI allows operators to configure:
- Blocked params (comma-separated, stripped from outgoing requests)
- Allowed params (comma-separated, re-added after denylist stripping)
- Auto-learn toggle (per-provider, enables auto-learning from 400 errors)

Wired into ProviderDetailPageClient.tsx between the Playground panel
and the Modals section.

Issue: #6625
PR: #6649

* feat(ui): add model-level param filter fields in compat popover

Extend ModelCompatPopover with Blocked params and Allowed params
text inputs for model-level denylist/allowlist overrides.

Model-specific block/allow data is persisted via the param-filters
API endpoint (PUT /api/providers/:id/param-filters) with the model
scope under the models key.

Both ModelRow and PassthroughModelRow now pass providerId and modelId
to the popover.

Issue: #6625
PR: #6649

* chore: gitignore .claude-flow/

* fix(param-filters): review follow-ups — auth gate, error sanitization, Zod body validation, typecheck, file-size, i18n keys

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* chore(param-filters): drop unrelated main-drift from the fork branch (deps/electron/proxy files belong to #6620/#6605/#6588, not this PR)

* refactor(param-filters): split oversized functions — keep complexity gate at baseline

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* refactor(param-filters): decompose config parser helpers — keep cognitive-complexity gate at baseline

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(changelog): restore sibling #6648 bullet eaten by merge auto-resolve + re-insert #6649 entry

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
2026-07-08 14:55:00 -03:00
PizzaV
2d9f85d587 fix(mimocode): handle 400 with cooldown + account rotation (#6648)
* fix(electron): bump electron 42→43 + build better-sqlite3 from source (ABI 148) (#6605)

fix(electron): bump electron 42→43 + rebuild better-sqlite3 from source against the Electron ABI (148).

Electron 43 raises NODE_MODULE_VERSION to 148; better-sqlite3@12.11.1 has no electron-v148 prebuild, so the packaged app died with 'Nenhum driver SQLite disponível'. prepare-electron-standalone now compiles better-sqlite3 from source against the electron headers into build/Release (where 'bindings' resolves it). Validated by Electron Package Smoke (green) + local (node_register_module_v148).

Supersedes #6378. (--admin: the only reds are SonarQube/SonarCloud failing on a coverage-report artifact digest-mismatch — a GitHub Actions infra flake, not this diff; Sonar is green on main and the diff touches only the electron build.)

* deps: bump the development group across 1 directory with 6 updates (#6588)

deps: bump the development group (6 updates). Rebased onto current main; all checks green after the electron-smoke fix (#6605).

* fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump (#6620)

fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump.

undici 8.6+ changed ProxyAgent to forward plain-HTTP via request-proxy instead of CONNECT, breaking OAuth refresh through a connection proxy (501). proxyDispatcher now passes proxyTunnel:true. Validated: Unit Tests 3/8 (the OAuth-proxy test) green, new regression test green (fails without the fix on undici 8.7), SonarQube green.

Supersedes #6380. (--admin: the only red is Electron Package Smoke failing on a next-build artifact 'digest-mismatch' — a GitHub Actions infra flake corrupting the asar ('file data stream has unexpected number of bytes'); the better-sqlite3 rebuild itself succeeded (gyp ok) and the electron path is unchanged from #6605 which passed the smoke. Not this diff.)

* fix(mimocode): handle 400 with cooldown + account rotation

Treat HTTP 400 responses the same as 429: mark the account on cooldown
and continue to the next fingerprint/proxy. Previously, 400 fell through
to markSuccess and returned immediately, so only 1 of N accounts was ever
tried per request.

Refs: #5925

* chore(mimocode): drop unrelated dependency/electron drift from PR #6648's stale fork

package.json/package-lock.json (bun/eslint-config-next/cyclonedx bumps), electron/package.json,
electron/package-lock.json, open-sse/utils/proxyDispatcher.ts, prepare-electron-standalone.mjs
and tests/unit/proxy-dispatcher-family.test.ts were already present in the contributor's single
commit but are unrelated to the mimocode 400-handling fix — restored to release/v3.8.47's
versions so the PR stays scoped to open-sse/executors/mimocode.ts.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(mimocode): classify 400 body before rotating — rate-limit-text 400s rotate, malformed 400s fail fast (#2101/#4976 guard)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* refactor(mimocode): extract auth-retry + 429/400 gating helpers — keep execute() under the cognitive-complexity gate

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: pizzav-xyz <pizzav-xyz@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
2026-07-08 14:08:48 -03:00
Diego Rodrigues de Sa e Souza
d0c775564a docs(claude): atualiza nomes da família de skills review/triage/implement (Hard Rule #21) (#6663) 2026-07-08 11:31:33 -03:00
Diego Rodrigues de Sa e Souza
a14bbccf17 chore(deps): bump omniglyph to ^1.0.2 (security: ReDoS fixes) (#6661)
The lockfile pinned omniglyph@1.0.0, which carries the polynomial-ReDoS regex
paths fixed in 1.0.1/1.0.2 (all upstream CodeQL alerts resolved). Bump the range
to ^1.0.2 and refresh the lock so `npm ci` installs 1.0.2. No change to the
omniglyph engine behavior — 1.0.1/1.0.2 touched only regex hot paths and docs;
the dependency tree is unchanged (gpt-tokenizer ^3.4.0).

Co-authored-by: diegosouzapw <souzamiriamrodrigues790@gmail.com>
2026-07-08 11:31:18 -03:00
Diego Rodrigues de Sa e Souza
fd7e4c10e5 feat(compression): omniglyph engine (context-as-image, Fable 5 direct) — stack + single mode (#6556)
* feat(compression): dependência omniglyph (file:) + smoke de import

* feat(compression): engine omniglyph — contexto-como-imagem com gates fail-closed

* fix(compression): omniglyph adapter fail-open no transform (try/catch)

* feat(compression): registra omniglyph no registry e catálogo (single mode, stackPriority 90)

* feat(compression): modo único omniglyph (async), selecionar o modo é o enable

* feat(compression): plumbing supportsVision + providerTransport até os engines

* feat(compression): estimador de tokens image-aware — modo stacked mantém a saída do omniglyph

* docs(compression): corrige comentário do prefixo base64 no decode PNG (64 chars)

* feat(compression): registra omniglyph nas listas de modo/engine (db, combo, deriveDefaultPlan, mcp)

* feat(dashboard): dedicated OmniGlyph engine screen (context-as-image)

Adds a per-engine detail page at /dashboard/context/omniglyph, alongside the
other compression engines in the sidebar. Four sections: the economics (measured
savings), a REAL before→after (dense text vs the rendered PNG page, not a mockup),
the fail-closed gate flow, and the enable control wired to /api/settings/compression
(preview engine, off by default). Sidebar entry + i18n label across all locales.

* chore(compression): consume published omniglyph@^1.0.0 from the npm registry

Replaces the local file: dependency used during the preview phase — npm ci
now resolves omniglyph from the registry with integrity, unblocking CI.

* fix(compression): satisfy v3.8.47 quality gates for the omniglyph engine

- dependency-allowlist: approve omniglyph (own package, published from
  diegosouzapw/OmniGlyph; supply-chain review done by the maintainer)
- ladder maps (#6533 guard): rank omniglyph 80 (stackPriority 90, runs after
  every text engine) with expectedReductionFactor 0.35 (measured 0.23-0.33)
- drop the two explicit any casts in omniglyph tests (no-explicit-any is
  error-level in tests since #6218)

* chore(compression): rebaseline strategySelector for the omniglyph mode dispatch

+18 lines of cohesive dispatch/type wiring at the existing mode chokepoints
(sync no-op + async single-mode branch + providerTransport on the options
types) — not extractable without hiding the dispatch boundary, mirroring the
prior compression rebaselines. Also drops an unused eslint-disable directive
in image-aware-tokens.test.ts (warning-level red under --max-warnings 0).

* chore(quality): register inherited base tests in stryker tap.testFiles

masked-200-exhaustion-fallback-6427 and headroom-codex-quota-snapshot-6379
arrived via the base merge without their stryker registration —
check:mutation-test-coverage --strict requires covering tests to be listed.

* refactor(compression): keep omniglyph wiring under the complexity gate

- extract the async single-mode resolution to engines/omniglyphSingleMode.ts
  (runCompressionAsync was at complexity 17 after the mode branch; back <=15)
- split OmniglyphContextPageClient into section components (was 161 lines in
  one function; every function now under the 80-line cap)
- complexity baseline 2052->2053: the +1 is inherited base drift (the ratchet
  does not run on fast-path merges — same pattern as the v3.8.44/46
  rebaselines); this PR's own code is measured complexity-net-zero

* chore(quality): register 3 more inherited base tests in stryker tap.testFiles

route-guard-middleware-local-only, combo-diagnostics-trace and
idempotency-fusion-collision arrived via the latest base merge without their
stryker registration (fast-path merges skip check:mutation-test-coverage).

* chore(quality): cognitive-complexity baseline 883->884 (inherited base drift)

check:cognitive-complexity measures 884 identically on the pristine
origin/release/v3.8.47 tip and on this HEAD — the PR itself is
cognitive-net-zero (single-mode resolution extracted to its own module,
page client split into section components). Same inherited-drift pattern
as the v3.8.4x release rebaselines.

---------

Co-authored-by: diegosouzapw <diegosouzapw@devbox.local>
2026-07-08 07:56:44 -03:00
Jillur Rahman
95e4bf72d4 fix(playground): accept a dashboard session for presets under REQUIRE_API_KEY (#6554)
Merged — thank you, @developerjillur! Accept a valid dashboard session for /api/playground/presets under REQUIRE_API_KEY (the Playground page authenticates via cookie, not an API key). Integrated into release/v3.8.47.
2026-07-08 01:36:12 -03:00
Jillur Rahman
ed0b9c73de perf(health): short-TTL cache for GET /api/monitoring/health (#6553)
Merged — thank you, @developerjillur! Short-TTL (1s) cache for the frequently-polled GET /api/monitoring/health, invalidated on DELETE (circuit-breaker reset). Integrated into release/v3.8.47.
2026-07-08 01:36:03 -03:00
Jillur Rahman
63d15bcb93 feat(combo): sanitized diagnostic trace on auto-combo terminal failure (#6545)
Merged — thank you, @developerjillur! Sanitized diagnostic trace on an auto-combo terminal failure (ids/reason-codes only, capped), plus an actionable reasoning-budget-exhausted message. Integrated into release/v3.8.47.
2026-07-08 01:35:55 -03:00
Jillur Rahman
8d59e1f660 fix(security): fail-closed CORS for cloud-agent management routes (#6543)
Merged — thank you, @developerjillur! Fail-closed CORS for the cookie/session-authed cloud-agent management routes (allowlist echo, credentials only for an explicitly allowlisted origin). Integrated into release/v3.8.47.
2026-07-08 01:35:46 -03:00
Jillur Rahman
899c40da67 fix(security): SSRF-guard provider validation probes (block cloud metadata) (#6542)
Merged — thank you, @developerjillur! SSRF-guards the provider-validation probes (block-metadata + no redirect) so a caller-controllable baseUrl can't relay to cloud metadata. Integrated into release/v3.8.47.
2026-07-08 01:35:38 -03:00
Jillur Rahman
2c42599e33 fix(security): loopback-gate /api/middleware/* (arbitrary JS via vm.Script) (#6541)
Merged — thank you, @developerjillur! Loopback-gates /api/middleware/* (arbitrary JS via vm.Script) for RCE parity with /api/plugins/*. Integrated into release/v3.8.47.
2026-07-08 01:35:29 -03:00
Jillur Rahman
db0830e60a fix(fusion): judge replayed a panel answer via idempotency-key collision (#6558)
Merged — thank you, @developerjillur! Namespaces the idempotency key by target provider/model + a messages digest so fusion panel/judge sub-requests can't collide on a shared client Idempotency-Key. Existing chatCore extracted-module tests were aligned to the composed-key contract. Integrated into release/v3.8.47.
2026-07-08 01:35:01 -03:00
Diego Rodrigues de Sa e Souza
639eedb1da fix(api): accept valid Codex connection edits instead of rejecting as Invalid request (#6562) (#6626) 2026-07-08 00:58:19 -03:00
Diego Rodrigues de Sa e Souza
ede77d1df0 fix(api): stop POST /api/keys hanging on the fire-and-forget Cloud sync (#6570) (#6624)
cloudEnabled defaults to true in settings.ts::getSettings() for any install
with no persisted settings row (every fresh install), so the create-key
handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a
real outbound fetch() to CLOUD_URL via syncToCloud(). When that endpoint is
unset/unreachable/slow, the HTTP response blocked until the request settled
or timed out (20-90s+), unlike sibling routes (regenerate, /api/combos) that
never touch this side effect.

syncKeysToCloudIfEnabled() is now dispatched fire-and-forget instead of
awaited; its internal try/catch already logs failures, so cloud sync still
runs in the background without blocking the response.
2026-07-08 00:33:56 -03:00
Diego Rodrigues de Sa e Souza
48e902c9d0 fix(startup): normalize non-Error throws + tolerate closed DB in instrumentation bootstrap (#6560) (#6622)
An update/restart could crash the whole server at boot with
TypeError: Cannot create property 'message' on string 'Database closed',
masking the real failure. driverFactory.ts's preInitSqlJs() cached its sql.js
WASM adapter per file path but never checked whether it had since been
closed by a racing gracefulShutdown/resetDbInstance; reusing the dead
handle made the next query throw sql.js's own raw string "Database closed"
straight out of instrumentation-node.ts's previously-unguarded
ensureDbInitialized() call. Next.js's registerInstrumentation() wrapper
unconditionally does err.message = ... on whatever register() rejects
with, and assigning .message on a primitive string throws in strict mode
-- that secondary TypeError is what actually crashed the process.

Fixed in two parts: preInitSqlJs() now evicts a closed cached adapter
instead of returning it, and a new ensureDbReadyForBoot() normalizes any
non-Error throw and retries once for a transient "database closed"
message before re-throwing anything else as a real Error.
2026-07-08 00:18:33 -03:00
Diego Rodrigues de Sa e Souza
fb3892b52e fix(auth): enforce API-key model/combo policy on the Codex Responses WebSocket bridge (#6564) (#6621)
The Codex Responses-over-WebSocket bridge authenticated the API key but
never called enforceApiKeyPolicy(), so a key restricted via
allowedModels/allowedCombos could still reach a direct Codex model
(e.g. gpt-5.5) through this transport, bypassing what the HTTP
/v1/responses path already enforces.

prepare() now builds an equivalent Request carrying an explicit
Authorization: Bearer <apiKey> header (the WS bridge's token normally
arrives via a query param) and calls enforceApiKeyPolicy() against the
client-requested model before any Codex-specific remapping or
credential selection.
2026-07-08 00:08:37 -03:00
KooshaPari
ca540c810d chore(open-sse): remove vestigial @ts-nocheck from usageTracking.ts (#6173)
chore(open-sse): remove vestigial @ts-nocheck from usageTracking.ts (#6173). Restores type-checking on the token-usage hot path under typecheck:core. Integrated into release/v3.8.47.
2026-07-07 23:58:56 -03:00
KooshaPari
fc01f53f94 chore(cli): harden empty catches in completion.mjs with env-gated error logging (#6257)
chore(cli): harden empty catches in completion.mjs with env-gated error logging (#6257). Reconstructed cleanly onto release/v3.8.47; env var documented. Integrated into release/v3.8.47.
2026-07-07 23:57:44 -03:00
Diego Rodrigues de Sa e Souza
78f05fc639 fix(startup): resolve AgentBridge MITM router key from existing OmniRoute key (#6403) (#6619)
AgentBridge's start/restart actions only ever checked an explicit apiKey
request field (never sent by the UI) and the ROUTER_API_KEY process env
var (unset unless manually exported), so startMitm() always spawned
server.cjs with an empty ROUTER_API_KEY and it hard-exited with
"no API key was provided". resolveRouterApiKey() now falls back to
pickApiKeyForInternalUse(), the same DB-backed selector already used by
the combo-health-check / cloud-sync-verify internal probes.
2026-07-07 23:27:34 -03:00
Diego Rodrigues de Sa e Souza
f8e179d479 fix(providers): send a Cloudflare-accepted Content-Type on Worker upload (#6416) (#6618) 2026-07-07 23:23:29 -03:00
Diego Rodrigues de Sa e Souza
318a5e5220 fix(startup): generate AgentBridge MITM certs for all 4 antigravity hosts (#6494) (#6617)
generateCert() hard-coded a single SAN entry (daily-cloudcode-pa.googleapis.com)
while server.cjs terminates TLS locally for all 4 antigravity/cloudcode-pa hosts,
so 3 of the 4 hosts served a cert whose CN/SAN didn't match and MITM interception
failed for them. Source the host list from the existing authoritative
ANTIGRAVITY_TARGET.hosts registry instead of a second hard-coded copy.
2026-07-07 23:17:22 -03:00
Diego Rodrigues de Sa e Souza
7d67fc705c fix(resilience): fall back on a 200 masking in-body credit exhaustion (#6427) (#6616)
`validateResponseQuality()` only inspected a response's top-level `error`
field when `choices` was also missing/empty (the narrower #3424 case), so a
masked HTTP 200 that echoed a non-empty stub `choices` alongside a structured
error object — or a known exhaustion phrase like "insufficient credits" /
"quota exceeded" in the error envelope — slipped through as valid, and a
`priority` combo kept hammering the exhausted target instead of failing
over.

The check now inspects the error envelope (top-level `error` object, or a
bounded exhaustion-phrase match against error.message/code/type and
top-level message/detail) unconditionally, before any shape-specific
branch — never against `choices[].message.content`, so legitimate
completions that merely mention "quota" in prose are not misclassified.

Regression guard: tests/unit/masked-200-exhaustion-fallback-6427.test.ts
2026-07-07 22:41:25 -03:00
Diego Rodrigues de Sa e Souza
908e3bef38 fix(compression): add adaptive-ladder rankings for non-default catalog engines (#6533) (#6615) 2026-07-07 22:39:14 -03:00
Diego Rodrigues de Sa e Souza
533016af36 fix(providers): backfill #6454 CHANGELOG bullet + 11-member fusion regression guard (#6614)
The fusion quorum-clamp/failure-detail root cause reported in #6454 was
already fixed and merged via #6521 (open-sse/services/fusion.ts already
carries Math.max(1, cfg.minPanel) + per-member failure reasons on this
branch). That merge never landed a CHANGELOG bullet for #6454 itself.

Backfills the missing bullet and adds a regression test at the exact
repro scale (11-member fusion-free-style panel, 2 cooling / 9 healthy)
to lock in that a cooling minority no longer sinks a healthy majority,
while a genuinely all-failed panel still returns the documented 503.
2026-07-07 22:38:26 -03:00
Diego Rodrigues de Sa e Souza
f4cd3e8c80 fix(api): serialize tool-call args correctly through /anthropic translation (#6459) (#6609)
appendToolCallArgumentDelta() treated any non-string incoming fragment as
empty, silently dropping tool-call arguments delivered as an already-parsed
JSON object/array (a non-conformant shape some upstreams emit for
tool_calls[].function.arguments) instead of JSON-encoding them. This left
tool_use.input empty on the /anthropic streaming path and opened the door to
downstream [object Object] string coercion once buffers were concatenated.
Now JSON.stringify()s the non-string fragment instead of discarding it.
2026-07-07 21:48:27 -03:00
Diego Rodrigues de Sa e Souza
978e92e104 fix(providers): honor fusion config.judgeModel for final synthesis (#6455) (#6607)
The fusion single-survivor degrade path (added for #6454) returned the
lone panel answer directly whenever only one panelist succeeded, ignoring
an explicitly configured judgeModel. With default minPanel=2 and a 2-model
panel, any single flaky panelist forced this path every request, so the
configured judge never ran and the response .model reflected a panel member.

The judge is now still invoked to synthesize a lone surviving answer when
judgeModel is explicitly configured; the direct-answer shortcut is kept only
for the implicit case (no judgeModel, judge defaults to panel[0]).
2026-07-07 21:48:02 -03:00
Diego Rodrigues de Sa e Souza
c1e3590da7 fix(providers): keep image/diffusion models out of the chat models catalog (#6457) (#6606) 2026-07-07 21:45:15 -03:00
Diego Rodrigues de Sa e Souza
ebdfe727a6 fix(test): replace tautology in playground-api-tab + make test-masking catch it (#6404) (#6603)
playground-api-tab.test.tsx's SSE test always took the disabled-button branch
(the fetch mock returned an empty model list) and asserted a tautology instead
of exercising the SSE path it claims to verify. The test now selects a real
model to enable Send, asserts it is actually enabled, and asserts the streamed
SSE content reached the response editor.

check-test-masking.mjs's tautology subcheck only compares base-vs-HEAD counts
within a PR's own diff and no-ops entirely outside PR context (no
GITHUB_BASE_SHA/REF) -- so a tautology merged once, or checked with a bare
local run, stayed invisible forever after. Added an always-on absolute-floor
scan (scanBareTautologies/countBareTautologies) over every tracked test file,
scoped to the bare expect(true).toBe(true)/assert.equal(1,1) patterns that
have zero legitimate uses in this codebase -- deliberately excluding
assert.ok(true), which has ~15 pre-existing verified-legitimate
try/catch-fallback uses and stays on the lenient diff-only path.
2026-07-07 21:18:18 -03:00
Diego Rodrigues de Sa e Souza
f570960958 fix(oauth): persist and reuse rotated Codex OAuth refresh token (#6352) (#6602) 2026-07-07 21:08:37 -03:00
Diego Rodrigues de Sa e Souza
c51eed786c fix(resilience): thread connection snapshot into headroom Codex quota fetch (#6379) (#6600)
orderTargetsByHeadroom already loaded the per-connection DB snapshot (with
decrypted credentials) via expandTargetsByQuotaAwareConnections, but discarded
it before calling getSaturation. For Codex, fetchCodexSaturation forwards
straight to fetchCodexQuota(connectionId, connection), which needs the
connection object (or a prior registerCodexConnection() call that never
happens before headroom ranking runs) to read accessToken. Without it,
fetchCodexQuota returned null for every candidate, saturation failed open to
0 across the board, and headroom ranking fell back to the original combo
order regardless of actual free quota.

getSaturation() and the headroom SaturationFetcher seam now accept and thread
the loaded connection snapshot through to fetchCodexQuota.

Regression guard: tests/unit/headroom-codex-quota-snapshot-6379.test.ts
(seeds two real Codex connections in a throwaway SQLite DB with a fake
upstream fetch, confirms RED on unfixed code, GREEN after the fix).
2026-07-07 21:02:19 -03:00
Diego Rodrigues de Sa e Souza
6d649d3d3f fix(dashboard): size the web-session cookie modal to fit on 1080p (#6265) (#6601) 2026-07-07 21:01:45 -03:00
Diego Rodrigues de Sa e Souza
cc5f596b5f fix(logger): tolerate write to removed DATA_DIR so tests don't crash on teardown (#6360) (#6599) 2026-07-07 21:01:11 -03:00
Diego Rodrigues de Sa e Souza
c06392c533 fix(providers): include custom models in Free Provider Rankings filters (#6368) (#6598) 2026-07-07 21:00:41 -03:00
Diego Rodrigues de Sa e Souza
d76153c7bb fix(providers): stop cloudflare-ai from silently dropping image content parts (#6390) (#6597) 2026-07-07 20:59:24 -03:00
Diego Rodrigues de Sa e Souza
6812b62f4a docs(changelog): add missing #6304 and #6240 bug-fix bullets to v3.8.47 (#6596) 2026-07-07 20:50:11 -03:00
Chirag Singhal
687a5aefc0 fix(providers): reject image-only models on /v1/chat/completions with clear error (#6457) (#6525)
fix(providers): reject image-only models on /v1/chat/completions with a clear error (#6457) Integrated into release/v3.8.47. (thanks @chirag127)
2026-07-07 20:36:41 -03:00
Chirag Singhal
454d58a41d fix(providers): honor fusion minPanel=1 and surface per-member failures in fusion 503 (#6454) (#6521)
fix(providers): honor fusion minPanel=1 and surface per-member failures in fusion 503 (#6454) Integrated into release/v3.8.47. (thanks @chirag127)
2026-07-07 20:36:36 -03:00
Chirag Singhal
1b6a1f08c7 fix(api): return 415 when /v1/chat/completions receives non-JSON Content-Type (#6414) (#6513)
fix(api): return 415 on /v1/messages for non-JSON Content-Type via requireJsonContentType middleware (#6414) Integrated into release/v3.8.47. (thanks @chirag127)
2026-07-07 20:36:32 -03:00
Chirag Singhal
fc8459faa3 fix(autoCombo): exclude paid models from fusion candidate pools when hidePaidModels=true (#6328) (#6550)
fix(autoCombo): exclude paid-tier auto/* ids from the catalog when hidePaidModels=true (#6328). Integrated into release/v3.8.47. (thanks @chirag127)
2026-07-07 20:19:43 -03:00
Chirag Singhal
d180ac1244 fix(dashboard-api): apply hidePaidModels to /api/models + openrouter-catalog + test endpoints (#6328) (#6552)
fix(dashboard-api): apply hidePaidModels to /api/models + openrouter-catalog + test endpoints (#6328). Integrated into release/v3.8.47. (thanks @chirag127)
2026-07-07 20:19:17 -03:00
Chirag Singhal
e8b5aefb5a fix(compression): surface fallback reasons in preview response (#6461) (#6519)
fix(compression): surface fallback reasons in preview response (#6461). Integrated into release/v3.8.47. (thanks @chirag127)
2026-07-07 20:17:34 -03:00
Chirag Singhal
8407a24aef fix(backup): exclude paid models from JSON export/backup when hidePaidModels=true (#6328) (#6551)
fix(backup): exclude paid models from JSON export/backup when hidePaidModels=true (#6328) Integrated into release/v3.8.47. (thanks @chirag127)
2026-07-07 20:10:41 -03:00
Chirag Singhal
1438bfb509 fix(models): apply hidePaidModels to synced/custom/alias-backed/managed-fallback loops (#6328) (#6549)
fix(models): apply hidePaidModels to synced/custom/alias-backed/managed-fallback loops (#6328) Integrated into release/v3.8.47. (thanks @chirag127)
2026-07-07 20:10:36 -03:00
Chirag Singhal
529ebde7a3 fix(api): add explicit HEAD handler for /v1/models to prevent ~6s hang (#6400) (#6517)
fix(api): add explicit HEAD handler for /v1/models to prevent ~6s hang (#6400) Integrated into release/v3.8.47. (thanks @chirag127)
2026-07-07 20:10:31 -03:00
Chirag Singhal
1a87c90ff7 fix(providers): fail fast on empty auto-combo pool instead of 15s timeout (#6458) (#6546)
fix(providers): fail fast on empty auto-combo pool instead of 15s timeout (#6458). Integrated into release/v3.8.47. (thanks @chirag127)
2026-07-07 20:08:51 -03:00
Chirag Singhal
e242321f5c fix(compression): honor UI-toggled engines in stackedPipeline dispatch + surface substitution (#6463) (#6534)
fix(compression): honor UI-toggled engines in stackedPipeline dispatch + surface substitution (#6463). Integrated into release/v3.8.47. (thanks @chirag127)
2026-07-07 20:08:27 -03:00
Diego Rodrigues de Sa e Souza
fd4133df82 fix(providers): spawn Auggie CLI with shell:true on win32 (#6304) (#6510)
* fix(providers): spawn Auggie CLI with shell:true on win32 (#6304)

* chore: sync CHANGELOG to release tip (#6510; bullet re-added at merge)
2026-07-07 20:08:16 -03:00
Chirag Singhal
3aeb87be7b fix(api): return 400 for missing/invalid messages before model resolution (#6402) (#6515)
fix(api): return 400 for missing/invalid messages before model resolution (#6402). Integrated into release/v3.8.47. (thanks @chirag127)
2026-07-07 20:07:48 -03:00
Diego Rodrigues de Sa e Souza
9450e03b1e fix(api): exempt test-model requests from Output Styles injection (#6240) (#6511)
* fix(api): exempt test-model requests from Output Styles injection (#6240)

Root cause: handleChatCore's Phase 4A Output Styles injection (chatCore.ts)
was gated only by the operator's global compression.enabled switch,
independent of the per-request x-omniroute-compression header. The
dashboard 'Test model' action (modelTestRunner.ts) never sent that header,
so a globally-enabled Output Style (e.g. 'Ultra terse') always leaked its
system-prompt injection into a plain connection test.

Fix: skip Output Styles injection when the request explicitly opts out via
x-omniroute-compression: off, and always send that header from
buildInternalChatRequest / buildInternalRerankRequest.

Regression guard: tests/integration/test-model-compression-off-6240.test.ts,
tests/unit/model-test-runner-compression-off-6240.test.ts

* chore: sync CHANGELOG to release tip (#6511; bullet re-added at merge)
2026-07-07 20:07:30 -03:00
Chirag Singhal
8f0447a54d fix(providers): size AddApiKeyModal for 1080p — drop inner max-h cap, widen to lg (#6265) (#6526)
fix(providers): size AddApiKeyModal for 1080p (#6265). Integrated into release/v3.8.47. (thanks @chirag127)
2026-07-07 20:07:07 -03:00
Chirag Singhal
1ba814799f fix(api): JSON 404 for unknown /anthropic/*, /v1beta/*, /openai/*, /metrics, /debug, /.env (#6405 follow-up) (#6516)
fix(api): JSON 404 for unknown root routes /anthropic/*, /openai/*, /metrics, /debug, /.env (#6405 follow-up) Integrated into release/v3.8.47. (thanks @chirag127)
2026-07-07 20:02:26 -03:00
Chirag Singhal
07eb2ecdd9 fix(providers): enrich model_cooldown 429 body with retry_after ISO + credential count (#6460) (#6523)
fix(providers): enrich model_cooldown 429 body with retry_after ISO + credential count (#6460). Integrated into release/v3.8.47. (thanks @chirag127)
2026-07-07 19:49:57 -03:00
Chirag Singhal
5e5fb61fcc feat(plugins): add langfuse plugin (#6577)
feat(plugins): add langfuse example plugin Integrated into release/v3.8.47. (thanks @chirag127)
2026-07-07 19:32:28 -03:00
Chirag Singhal
18d0659426 fix(tests): replace expect(true) tautology in playground-api-tab (#6404) (#6548)
fix(tests): replace expect(true) tautology in playground-api-tab (#6404) Integrated into release/v3.8.47. (thanks @chirag127)
2026-07-07 19:32:24 -03:00
Chirag Singhal
da841f1a16 docs(readme): cross-link CodeWebChat as editor-side companion (#6189) (#6547)
docs(readme): cross-link CodeWebChat as editor-side companion (#6189) Integrated into release/v3.8.47. (thanks @chirag127)
2026-07-07 19:32:19 -03:00
Chirag Singhal
5ab442fcd3 fix(providers): warn when config.judgeModel is set on a non-fusion combo (#6455) (#6532)
fix(providers): warn when config.judgeModel is set on a non-fusion combo (#6455) Integrated into release/v3.8.47. (thanks @chirag127)
2026-07-07 19:32:15 -03:00
Diego Rodrigues de Sa e Souza
58bd527bb9 fix(resilience): recoverable 403 for no-credential providers (#6315) (#6508)
fix(resilience): recoverable 403 for no-credential providers (#6315). Integrated into release/v3.8.47.
2026-07-07 18:34:34 -03:00
Diego Rodrigues de Sa e Souza
f686e97d6c fix(api): remove duplicate origin check causing LAN 403 on health-autopilot actions (#6277) (#6507)
fix(api): remove duplicate origin check causing LAN 403 on health-autopilot actions (#6277). Integrated into release/v3.8.47.
2026-07-07 18:14:40 -03:00
Diego Rodrigues de Sa e Souza
de9d748dac fix(pricing): persist sync status across module instances (#6325) (#6505)
fix(pricing): persist sync status across module instances (#6325). Integrated into release/v3.8.47.
2026-07-07 18:04:59 -03:00
Diego Rodrigues de Sa e Souza
318b0b01d2 fix(cli): surface a diagnostic instead of a silent hang on serve readiness timeout (#6321) (#6504)
fix(cli): surface a diagnostic instead of a silent hang on serve readiness timeout (#6321). Integrated into release/v3.8.47.
2026-07-07 17:46:07 -03:00
Diego Rodrigues de Sa e Souza
359fc03c22 fix(providers): strip reasoning_effort/reasoning from grok-cli requests (#6288) (#6503)
fix(providers): strip reasoning_effort/reasoning from grok-cli requests (#6288). Integrated into release/v3.8.47.
2026-07-07 17:20:25 -03:00
Diego Rodrigues de Sa e Souza
3a1658d070 fix(providers): stop Antigravity false quota-exhausted (#6295) (#6502)
fix(providers): stop Antigravity false quota-exhausted (#6295). Integrated into release/v3.8.47.
2026-07-07 17:07:13 -03:00
Diego Rodrigues de Sa e Souza
eefb5097f7 chore(release): sync main (v3.8.46 close) into release/v3.8.47 — parallel-cycle sync-back 2026-07-07 15:45:55 -03:00
Diego Rodrigues de Sa e Souza
9f66316c4e feat(quality): validate-release-green --full-ci — reproduce the ci.yml static gate set (P0) (#6583)
* feat(quality): validate-release-green --full-ci reproduces the ci.yml static gate set

The curated HARD/DRIFT lists in validate-release-green were a hand-maintained
subset — v3.8.46 leaked 11 static base-reds (route-validation:t06, docs-symbols,
bundle-size --ratchet, test-masking, file-size, …) to the release PR because they
live only in the ci.yml gate jobs, costing ~2h of layered CI. --full-ci reads
ci.yml itself and runs every npm run check:* / lint from the lint / quality-gate /
quality-extended / docs-sync-strict / pr-test-policy jobs (-- ratchet flags
preserved; test-masking against GITHUB_BASE_REF=main; skips pr-evidence +
codeql-ratchet which can't run in a local working-tree pre-flight). Reading from
ci.yml keeps the set current as gates are added. Also wired into
nightly-release-green so a static base-red opens a tracking issue the night it
lands. Regression guard: +5 extractCiGates cases (18/18 pass).

* chore(quality): add 3 covering tests to stryker tap.testFiles (pre-existing drift on release/v3.8.47)

check:mutation-test-coverage (fast-gates) flagged 3 unit tests that cover mutated
modules but were missing from stryker.conf.json tap.testFiles — pre-existing drift
on release/v3.8.47, surfaced by this PR's CI. Adds codex-quota-selection-hydration
(auth.ts), combo-roundrobin-compat-fallback-6238 (circuitBreaker.ts), and
combo-rr-fallback-advance-948 (rrState.ts) so their mutant kills count.
2026-07-07 15:44:59 -03:00
Diego Rodrigues de Sa e Souza
d545719956 chore(release): sync main (v3.8.46 close) into release/v3.8.47 — parallel-cycle sync-back 2026-07-07 14:41:00 -03:00
Diego Rodrigues de Sa e Souza
b1d5f202c7 chore(release): open v3.8.47 development cycle
Parallel-cycle model (2026-07-04): cut from the frozen release/v3.8.46 tip at the
v3.8.46 release freeze so development continues immediately on v3.8.47 while the
captain closes v3.8.46. Bumps package.json x3 + openapi + lockfile to 3.8.47, adds
the '## [3.8.47] — TBD' living CHANGELOG section, and syncs the 42 i18n mirrors.
Closing v3.8.46 fixes reach this branch via the Phase 5 sync-back.
2026-07-07 07:58:13 -03:00
Diego Rodrigues de Sa e Souza
8fb3a2c379 chore(quality): v3.8.46 pre-flight — type test anys, rebaseline cycle drift, allowlist #6303 test consolidation
- Type 12 no-explicit-any errors in 3 new test files (real types, no masking):
  chat-early-schema-validation-6412, models-catalog-envkey-6406, zed-provider.
- Suppress MitmProxyTab.tsx no-html-link-for-pages (false positive: the link is
  an /api/settings/mitm cert download, an API route not a Next page).
- Allowlist tests/integration/v1-contracts-behavior.test.ts net -2 asserts: #6303
  moved embedding/image shape coverage to models-catalog-route + specialty tests.
- Rebaseline cycle drift measured on the release tip (captain fixes are net-zero,
  verified): cognitive 877->882, cyclomatic 2035->2050, file-size proxies/chat/
  ApiManagerPageClient/ProxyRegistryManager + models-catalog-route testcap +5.
- Fix stale CLAUDE.md: no-explicit-any is error (not warn) in tests/ since #6218.
2026-07-07 07:42:42 -03:00
Diego Rodrigues de Sa e Souza
ceff7054b1 fix(security): unbiased crypto digits for the doubao synthetic device id (CodeQL js/biased-cryptographic-random)
randomNumericId builds a non-secret synthetic device/web id. The v3.8.45 switch
to crypto.getRandomValues closed js/insecure-randomness but 'cryptoByte % 10' is
biased (256 is not a multiple of 10), tripping js/biased-cryptographic-random.
Draw each digit by rejection sampling — discard bytes in the biased tail so the
remaining range divides evenly — giving a uniform distribution (verified) while
staying crypto-backed.
2026-07-07 07:42:12 -03:00
Diego Rodrigues de Sa e Souza
a533a312df fix(agentSkills): generator honors an absolute outputDir (#6366 regression)
#6366 switched the output base from path.resolve to path.join(process.cwd(),
outputDir) to keep Turbopack's static analyzer from tracing the project root.
path.join mangles an absolute outputDir (a tmp dir in the generator tests) into
cwd/tmp/…, so apply mode reported success while writing nothing at the expected
path. Guard with path.isAbsolute — absolute paths pass through, the relative
production case ('skills') keeps the Turbopack-friendly join form. The existing
agentSkills-generator suite is the regression guard (now 21/21).
2026-07-07 07:41:53 -03:00
Diego Rodrigues de Sa e Souza
d776a42324 fix(api): invalidate /v1/models + specialty catalogs on DB writes; fix duplicated headers and 500 replayed as 200 (#6408)
The #6408 request-shape-keyed TTL cache around getUnifiedModelsResponse was not
keyed by DB/settings state, so a write followed by a read within the ~1.5s TTL
replayed the pre-write catalog (dropping newly-eligible models like codex/gpt-5.5
and every specialty catalog routed through it since #6303). Fold a
modelCatalogCacheVersion (bumped by invalidateDbCache, already called on every
settings/connection/combo/pricing write) into the cache so a state change forces
an immediate miss; merge response headers through a real Headers instance
(fixes X-Request-Id duplication); carry and replay status end-to-end (a mid-build
500 was replayed as 200). Tests call the existing __resetCatalogBuilderRunsForTest
hook in setup, matching v1-models-concurrent-6408.
2026-07-07 07:41:20 -03:00
Diego Rodrigues de Sa e Souza
4f294c0cca feat(sse): exclude paid-only models from auto/* candidate pool when hidePaidModels is on (#6512) (#6518)
Exclude paid-only models from the auto/* candidate pool when hidePaidModels is on (#6512). Integrated into release/v3.8.46; fixed a phantom vitest guard, 4/4 green.
2026-07-07 01:02:35 -03:00
Diego Rodrigues de Sa e Souza
0d3d31c21f feat(sse): provider-family auto combos auto/glm, auto/minimax, auto/zai, auto/mimo, auto/gemma, auto/llama, auto/gemini (#6453) (#6509)
Provider-family auto combos (#6453). Integrated into release/v3.8.46; vitest autoCombo suite 11/11 green.
2026-07-07 01:00:12 -03:00
Dilnei
526048da5e fix(ui): prevent silent overwrite of existing API key connections on re-add (#6499)
Unique default connection name prevents silent overwrite of existing API-key connections. Integrated into release/v3.8.46 with a unit-tested helper; remaining file-size reds are pre-existing base-red drift.
2026-07-06 23:59:01 -03:00
Chirag Singhal
9a33cdac1d fix(compression): add intra-message dedup to session-dedup engine (#6467) (#6501)
Intra-message dedup for the session-dedup compression engine (#6467) + fallbackReason surfacing + fusion rate-limit detail. Integrated into release/v3.8.46 with a TDD regression.
2026-07-06 23:53:52 -03:00
Chirag Singhal
c3cef782ac fix(compression): unknown engine names surface validationErrors instead of silently falling back (#6485) (#6506)
Surface validationErrors for unknown stacked compression engines. Integrated into release/v3.8.46 with a TDD regression.
2026-07-06 23:51:01 -03:00
Jan Leon
958260a5c9 Add Codex reset-credit redemption flow (#6361)
Codex reset-credit redemption flow (#6361). Tests 56/56; kept release 'Banked Reset Credits' label (reverted cosmetic rename that broke 2 release tests). Integrated into release/v3.8.46.
2026-07-06 21:50:27 -03:00
hao3039032
f6b4926138 feat(glm): add team plan quota settings for glm-cn connections (#6351)
add GLM team plan quota settings (#6351). Tests 29/29; reconciled FormData with m365Tier; modal caps bumped for own growth. Integrated into release/v3.8.46.
2026-07-06 21:35:04 -03:00
Aditya Banerjee
e45e6c3e34 feat: add TinyFish Fetch support to web-fetch provider and update related documentation (#6349)
add TinyFish web-fetch/search provider + tool (#6349). Tests green (tinyfish suites + count-guard 170->171). Integrated into release/v3.8.46.
2026-07-06 21:32:15 -03:00
Swing Tempo
69d2b31930 Swingtempo/fixwindowscodex (#6312)
launch-codex spawns codex.cmd via shell on Windows (#6263 pattern). Added resolveCodexSpawn() helper + regression test (2/2), satisfying Hard Rule #18. Integrated into release/v3.8.46.
2026-07-06 21:20:52 -03:00
Xiangzhe
95708051a2 fix(codex): isolate Spark quota and stabilize quota UI (#6336)
isolate Spark quota + stabilize quota UI (#6336). Tests 44/44, no file-size drift from its files. Integrated into release/v3.8.46.
2026-07-06 21:18:58 -03:00
Chirag Singhal
b681308259 feat(api): add hidePaidModels setting to filter paid-only models from /v1/models catalog (#6495)
feat(api): add hidePaidModels setting (net +1/-0, test OK). Integrated into release/v3.8.46.
2026-07-06 21:17:29 -03:00
jmengit
17da3b6e09 fix(api-manager): preserve combos in model fallback (#6443)
fix(api-manager): preserve combos in fallback model picker (net +1/-0, test OK). Integrated into release/v3.8.46.
2026-07-06 21:16:56 -03:00
Jillur Rahman
27867e5af3 fix(providers): treat recoverable Antigravity/Cloud-Code 403s as project errors, not account bans (#6452)
fix(providers): treat recoverable Antigravity/CF 403 as retryable (net +1/-0, test OK). Integrated into release/v3.8.46.
2026-07-06 21:16:39 -03:00
Jillur Rahman
62a74faa4b fix(mitm): redact Set-Cookie in sanitizeHeaders to prevent session-token leak (#6451)
fix(mitm): redact Set-Cookie in sanitizeHeaders (net +1/-0, test OK). Integrated into release/v3.8.46.
2026-07-06 21:16:16 -03:00
Chirag Singhal
a03c22e5ed fix(api): accept mode:'caveman' + stacked default pipeline yields 0% (#6425) (#6439)
fix(api): /api/compression/preview accepts mode caveman + stacked-zero (#6425) (net +1/-0, test OK). Integrated into release/v3.8.46.
2026-07-06 21:12:58 -03:00
Diego Rodrigues de Sa e Souza
04335944ea feat(providers): add Zed hosted LLM aggregator (native-app sign-in) — NEEDS LIVE OAUTH VALIDATION (#6118)
add Zed hosted LLM aggregator native-app provider (port PR #2328). VPS-validated live by operator (Hard Rule #18); zed suites 15/15. OAuthModal cap 989->993 (own growth). Remaining file-size reds are pre-existing release base-red drift (rebaselined at release Phase 0). Integrated into release/v3.8.46.
2026-07-06 20:43:11 -03:00
Diego Rodrigues de Sa e Souza
3cc48edb35 fix(oauth): preserve Kiro IDC region in SSO-cache auto-import (#6113)
preserve Kiro IDC region in SSO-cache auto-import (port PR #2314). VPS-validated live by operator (Hard Rule #18); kiro-auto-import-idc 9/9. Integrated into release/v3.8.46.
2026-07-06 20:39:06 -03:00
Diego Rodrigues de Sa e Souza
58f53e3a35 feat(proxy): native proxy-pool round-robin / egress IP rotation (#6365) (#6395)
native proxy-pool round-robin / egress IP rotation (#6365) (net +1/-0, tests OK). Integrated into release/v3.8.46.
2026-07-06 19:31:44 -03:00
Diego Rodrigues de Sa e Souza
18fa0b2651 feat(providers): Gemini tool-calling end-to-end on /v1beta (#6222) (#6394)
Gemini tool-calling end-to-end on /v1beta (#6222) (net +1/-0, tests OK). Integrated into release/v3.8.46.
2026-07-06 19:31:12 -03:00
Diego Rodrigues de Sa e Souza
f5147d00f9 feat(providers): copilot-m365-web enterprise (work) tier support (#6334) (#6392)
copilot-m365-web enterprise (work) tier support (#6334) (net +1/-0, tests OK). Integrated into release/v3.8.46.
2026-07-06 19:30:56 -03:00
Diego Rodrigues de Sa e Souza
62b1bc9d05 feat(api): standardize effort + thinking request params (#6241) (#6398)
standardize effort + thinking request params (#6241) (net +1/-0, tests OK). Integrated into release/v3.8.46.
2026-07-06 19:30:40 -03:00
Diego Rodrigues de Sa e Souza
8db5a665d6 feat(combo): sequential 'pipeline' combo strategy (#6297) (#6396)
sequential 'pipeline' combo strategy (#6297) (net +1/-0, tests OK). Integrated into release/v3.8.46.
2026-07-06 19:29:59 -03:00
Diego Rodrigues de Sa e Souza
ecf3d3aa06 feat(ci): check:test-masking flags inline-reimplemented prod conditions (#6348) (#6393)
check:test-masking flags inline-reimplemented prod (#6348) (net +1/-0, tests OK). Integrated into release/v3.8.46.
2026-07-06 19:29:29 -03:00
Diego Rodrigues de Sa e Souza
c7c7d476a3 feat(sse): per-connection routing override (native vs CLIProxyAPI) (#6339) (#6383)
per-connection routing override native vs CLIProxy (#6339) (net +1/-0, tests OK). Integrated into release/v3.8.46.
2026-07-06 19:29:13 -03:00
Diego Rodrigues de Sa e Souza
b7ac5261a5 feat(dashboard): 'Open <host>' link in Add session cookie modal (#6268) (#6391)
'Open <host>' link in Add session cookie modal (#6268) (net +1/-0, tests OK). Integrated into release/v3.8.46.
2026-07-06 19:28:39 -03:00
Diego Rodrigues de Sa e Souza
c6a80071f1 feat(providers): add DigitalOcean AI as an OpenAI-compatible provider (#6373)
add DigitalOcean AI OpenAI-compatible provider (port PR #2417). Resolved registry conflict with hcnsec #6410; count-guard 169→170. Tests green. Integrated into release/v3.8.46.
2026-07-06 19:27:51 -03:00
Diego Rodrigues de Sa e Souza
437ca488b0 feat(providers): add Huancheng Public API (hcnsec) OpenAI-compatible regional provider (#6410)
add Huancheng Public API (hcnsec) OpenAI-compatible provider (port PR #2378) (net +1/-0, tests OK). Integrated into release/v3.8.46.
2026-07-06 19:25:45 -03:00
Diego Rodrigues de Sa e Souza
b67f2c58da fix(dashboard): disambiguate colliding passthrough model aliases (port from 9router#1850) (#6431)
disambiguate colliding passthrough model aliases (port #1850) (net +1/-0, test OK). Integrated into release/v3.8.46.
2026-07-06 19:24:59 -03:00
Diego Rodrigues de Sa e Souza
2ecaae7c40 fix(translator): preserve co-located functionResponse parts in gemini→openai (#6376)
preserve co-located functionResponse parts (port PR #2394) (net +1/-0, test OK). Integrated into release/v3.8.46.
2026-07-06 19:24:43 -03:00
Diego Rodrigues de Sa e Souza
259d9b0f38 fix(headroom): detect python managed by mise/pyenv/asdf/conda (port from 9router#2353) (#6382)
detect python managed by mise/pyenv/asdf/conda (port #2353) (net +1/-0, test OK). Integrated into release/v3.8.46.
2026-07-06 19:24:24 -03:00
Diego Rodrigues de Sa e Souza
bd65eeb045 fix(executors): strip client_metadata for NVIDIA requests (port from 9router#1887) (#6411)
strip client_metadata for NVIDIA (port #1887) (net +1/-0, test OK). Integrated into release/v3.8.46.
2026-07-06 19:24:08 -03:00
Diego Rodrigues de Sa e Souza
d415cc0216 fix(translator): strip thinking for NVIDIA glm-5.2 (port from 9router#2023) (#6413)
strip thinking for NVIDIA glm-5.2 (port #2023) (net +1/-0, test OK). Integrated into release/v3.8.46.
2026-07-06 19:23:43 -03:00
Diego Rodrigues de Sa e Souza
cfbc2c27c2 fix(translator): suppress </think> marker for Antigravity client (port from 9router#1061) (#6415)
suppress </think> marker for Antigravity (port #1061) (net +1/-0, test OK). Integrated into release/v3.8.46.
2026-07-06 19:23:27 -03:00
Diego Rodrigues de Sa e Souza
58bb2cd8c7 fix(executors): strip nested reasoning_content for Mistral (port from 9router#1649) (#6417)
strip nested reasoning_content for Mistral (port #1649) (net +1/-0, test OK). Integrated into release/v3.8.46.
2026-07-06 19:22:52 -03:00
Diego Rodrigues de Sa e Souza
4b1c1859f8 fix(executors): strip client_metadata on the OpenCode path (port from 9router#1442) (#6418)
strip client_metadata on OpenCode path (port #1442) (net +1/-0, test OK). Integrated into release/v3.8.46.
2026-07-06 19:22:27 -03:00
Diego Rodrigues de Sa e Souza
f0b085ebca fix(executors): inject reasoning_content for native Kimi provider (port from 9router#1480) (#6419)
inject reasoning_content for native Kimi (port #1480) (net +1/-0, test OK). Integrated into release/v3.8.46.
2026-07-06 19:22:02 -03:00
Diego Rodrigues de Sa e Souza
1636ace600 fix(executors): strip client context_management on 400 (port from 9router#1468) (#6420)
recover from client context_management 400 (port #1468) (net +1/-0, test OK). Integrated into release/v3.8.46.
2026-07-06 19:21:38 -03:00
Diego Rodrigues de Sa e Souza
7a7a72c6f4 fix(network): enable Happy Eyeballs on direct egress (port from 9router#1237) (#6423)
Happy Eyeballs on direct egress (port #1237). Integrated into release/v3.8.46.
2026-07-06 19:20:53 -03:00
Diego Rodrigues de Sa e Souza
465f0a0e83 fix(combo): advance round-robin pointer past the served model (port from 9router#948) (#6428)
combo round-robin advances pointer past served model (port #948). Test combo-rr-fallback-advance-948 (green). Integrated into release/v3.8.46.
2026-07-06 19:18:42 -03:00
Chirag Singhal
a73c6ca5fb fix(sse): reject non-string model with 400 before resolver (#6407) (#6433)
reject non-string model with 400 before resolver (#6407, 6/6). Reconciled with #6437 early schema validation on chat.ts. Integrated into release/v3.8.46.
2026-07-06 18:43:06 -03:00
Chirag Singhal
a7e0dddac4 fix(chat): validate scalar params before provider lookup (#6412) (#6437)
JSON 404 for unknown /api/* (#6424) + early scalar-param validation before provider lookup (#6412, 10/10). Integrated into release/v3.8.46.
2026-07-06 18:40:39 -03:00
Chirag Singhal
d12ac37d4d fix(api): reject non-JSON Content-Type on /v1/chat/completions with 415 (#6414) (#6434)
reject non-JSON Content-Type on /v1/chat/completions (#6414, 4/4). Integrated into release/v3.8.46.
2026-07-06 18:39:25 -03:00
Chirag Singhal
fa3a09cb43 fix(api): echo X-OmniRoute-Compression response header (#6422) (#6441)
echo X-OmniRoute-Compression header on completions routes (#6422, 6/6). Reconciled with #6429 body.model echo. Integrated into release/v3.8.46.
2026-07-06 18:38:12 -03:00
Chirag Singhal
d11bf528af fix(api): coalesce concurrent GET /v1/models to one builder run (#6408) (#6440)
coalesce concurrent GET /v1/models (#6408, 3/3). Integrated into release/v3.8.46.
2026-07-06 18:36:37 -03:00
Chirag Singhal
ac96c0dd02 fix(completions): echo requested body.model on /v1/completions to match x-omniroute-model header (#6429)
echo body.model on /v1/completions (5/5). Integrated into release/v3.8.46.
2026-07-06 18:35:25 -03:00
Chirag Singhal
6ea7c68e6e fix(api): env-var master keys see full /v1/models catalog (#6406) (#6436)
env-key master sees full /v1/models catalog (#6406, 2/2). Integrated into release/v3.8.46.
2026-07-06 18:35:01 -03:00
Chirag Singhal
c8e94d7a14 fix(chatCore): align non-streaming body.model with X-OmniRoute-Model header (#6426) (#6432)
align non-streaming body.model with X-OmniRoute-Model (#6426, 3/3). Integrated into release/v3.8.46.
2026-07-06 18:33:49 -03:00
Chirag Singhal
a2fabdde8f fix(api): return JSON 404 for unknown /v1/* routes (#6405) (#6435)
JSON 404 for unknown /v1/* routes (#6405). Test tests/unit/api/v1-catchall-json-404.test.ts (3/3). Integrated into release/v3.8.46.
2026-07-06 18:32:33 -03:00
Diego Rodrigues de Sa e Souza
2f4b793c3d fix(api): provider-models route — redirect→local-catalog fallback + custom-model merge (#6267, #6247) (#6449) 2026-07-06 18:17:02 -03:00
Diego Rodrigues de Sa e Souza
cd4a720b7e fix(providers): bound GitLab Duo tool-exchange prompt to avoid 422 (#6220) (#6446) 2026-07-06 18:16:49 -03:00
Diego Rodrigues de Sa e Souza
4a2172e569 fix(i18n): translate provider connection-status filter labels across locales (#6290) (#6448) 2026-07-06 18:16:35 -03:00
Diego Rodrigues de Sa e Souza
53aa6d9716 fix(providers): add redacted WS debug logging to copilot-m365-web (#6210) (#6447) 2026-07-06 18:16:22 -03:00
Diego Rodrigues de Sa e Souza
d98a31587e fix(resilience): combo falls back to compat-rejected healthy targets before 503 (#6238) (#6450) 2026-07-06 18:16:10 -03:00
Diego Rodrigues de Sa e Souza
f1a02f602b fix(startup): best-effort self-heal for corrupted Turbopack dev cache on Windows (#6289) (#6445) 2026-07-06 18:15:58 -03:00
Diego Rodrigues de Sa e Souza
c50a83a94d fix(providers): resolve qodercli via cliRuntime on Windows (#6263) (#6389) 2026-07-06 18:13:31 -03:00
Diego Rodrigues de Sa e Souza
76b1b04495 fix(sse): do not inflate probe-sized max_tokens in reasoning buffer (#6274) (#6388) 2026-07-06 18:13:18 -03:00
Diego Rodrigues de Sa e Souza
e3d29d1419 fix(cli): register reset-password subcommand + non-TTY stdin path (#6261, #6258) (#6387) 2026-07-06 18:13:06 -03:00
Diego Rodrigues de Sa e Souza
db7a6c2437 fix(db): migration safety abort — add bypass hint + memoize to stop cascade (#6260) (#6386) 2026-07-06 18:12:35 -03:00
Diego Rodrigues de Sa e Souza
6fff4d6df1 fix(auth): dedup Codex OAuth import by workspace AND user id (#6301) (#6385) 2026-07-06 18:11:37 -03:00
Diego Rodrigues de Sa e Souza
f60090b278 fix(providers): venice-web static-catalog fallback for models listing (#6269) (#6384) 2026-07-06 18:09:42 -03:00
Makcim Ivanov
0086f1772b fix(api): filter specialty model catalogs (#6303)
Derive specialty model catalogs from the unified catalog via a shared predicate filter. Integrated into release/v3.8.46.
2026-07-06 17:44:16 -03:00
Milan Soni
49795c24ef fix(api): dynamic import for MITM + fix Turbopack over-bundling warnings (#6366)
Dynamic MITM manager import on the agent-bridge route + Turbopack static-analyzer anchor in the skills generator (#6329). Integrated into release/v3.8.46.
2026-07-06 17:34:33 -03:00
jmengit
049bad494b fix(internal): use explicit internal key selection for dashboard probes (#6372)
Internal probes pick a management-scoped key (pickApiKeyForInternalUse); API-manager model-editor fallback catalog. Integrated into release/v3.8.46.
2026-07-06 17:20:53 -03:00
jordansilly77-stack
b796f2b457 feat(providers): link web session guide to provider site (#6316)
Add an Open-provider-site link to the web-session credential guide (+host-extraction test). Integrated into release/v3.8.46.
2026-07-06 17:19:47 -03:00
Vinayak Kulkarni
fb3da7ce98 fix(live-ws): reject on bind failure instead of crashing the process (closes #6324) (#6332)
LiveWS server rejects on bind failure instead of crash-looping the process (closes #6324). Integrated into release/v3.8.46.
2026-07-06 16:42:20 -03:00
Xiangzhe
8853b257aa fix(dashboard): trust provider topology live state (#6322)
Trust live provider-topology state on the Home dashboard. Integrated into release/v3.8.46.
2026-07-06 16:25:05 -03:00
backryun
0785cd2e55 feat(cerebras): add Gemma 4 31B model (#6331)
Add Cerebras Gemma 4 31B model + pricing + catalog test. Integrated into release/v3.8.46.
2026-07-06 16:16:27 -03:00
Diego Rodrigues de Sa e Souza
a0ab693c4b fix(docker): make MITM manager Turbopack stub opt-in so npm/Electron/VPS bundle the real manager (#6344) (#6374)
* fix(docker): make @/mitm/manager Turbopack stub opt-in (OMNIROUTE_MITM_STUB=1) so npm/Electron/VPS builds bundle the real manager (#6344)

v3.8.45 flipped the production bundler default to Turbopack. next.config.mjs
aliased @/mitm/manager to the Docker-only degraded stub unconditionally, which
was harmless while Docker was the only Turbopack consumer but shipped the stub
to every npm/Electron/VPS artifact once Turbopack became the default — breaking
Agent Bridge start with 'MITM manager stub reached at runtime'. The alias is now
gated on OMNIROUTE_MITM_STUB=1 (set only by the Dockerfile) via the shared
scripts/build/mitm-stub-flag.mjs helper.

Regression guard: tests/unit/mitm-stub-alias-6344.test.mjs (4).

* test(next-config): align mitm-manager alias assertion with opt-in stub (#6344)

The existing test asserted the alias was always present; #6344 makes it opt-in
(OMNIROUTE_MITM_STUB=1). Default build now asserts no alias, plus a new env-matrix
test covering both the default (no stub) and Docker (stub) cases.

* docs(changelog): restore #6359 bullet eaten by merge auto-resolve
2026-07-06 15:20:40 -03:00
Diego Rodrigues de Sa e Souza
a69547a7f5 fix(sse): coerce tool-call function schema root type:null to "object" (#6359) (#6375)
Clients like the Codex app emit parameters:{type:null,...} for some tools;
OpenAI-compatible upstreams reject with '400 Invalid schema for function ...:
schema must be a JSON Schema of type object, got type null'. toolSchemaSanitizer
already dropped the null; it now re-adds the mandatory root object type (plus
empty properties / open additionalProperties when absent). Combinator roots
(anyOf/oneOf/allOf) and explicit root types are preserved.

Regression guard: 5 new cases in tests/unit/tool-schema-sanitizer.test.mjs.
2026-07-06 14:58:21 -03:00
Diego Rodrigues de Sa e Souza
0776f83fd0 docs(changelog): v3.8.46 bullets for the release-process improvements (#6319, #6327, #6347) (#6362) 2026-07-06 11:14:53 -03:00
Diego Rodrigues de Sa e Souza
f3d285ba90 fix(ci): sync-next-cycle — widen git() maxBuffer (ENOBUFS on >1MiB CHANGELOG) + propagate finalized section to i18n mirrors (#6327)
Two defects found live in the v3.8.45 Phase 5 run (2026-07-06):
- execFileSync default 1 MiB maxBuffer crashed on git show origin/main:CHANGELOG.md
- the i18n resync only synced the [NEXT] (TBD) section, leaving the just-shipped
  finalized section as '— TBD' in all 42 mirrors; now also syncs [prevVersion]
  bounded by the heading below it (new pure helper versionAfter, unit-tested)
2026-07-06 10:50:41 -03:00
Diego Rodrigues de Sa e Souza
c0430342c6 test(ci): quarantine concurrency-sensitive flakes into a serial pass (#6347)
* test(ci): quarantine concurrency-sensitive flakes into a serial pass (tests/unit/serial/)

glm-coding-plan-monthly-3580, quota-division-blocks and provider-health-autopilot
fail under --test-concurrency>1 CPU contention but pass isolated (v3.8.45 release
benchmark: this class cost ~28min re-runs per CI round). They now live in
tests/unit/serial/ and run in a dedicated --test-concurrency=1 step appended to
every runner (test:unit, :ci, :ci:shard, :fast, :shard:1/2, coverage:runner —
sharded variants shard the serial pass too, so concurrent shard jobs never
self-collide). Discovery + TIA gates know the new glob; stryker.conf.json path
updated. Guard: tests/unit/test-serial-quarantine.test.ts (4 tests).

* test(ci): quarantine combo-health-autopilot too (async logger writes after test end under load)

Fresh evidence from this PR's own CI: all 3 subtests pass but an async log write
lands after the test ends (ENOENT app.log -> uncaughtException) — same
concurrency-flake family. Moved to tests/unit/serial/, imports adjusted,
stryker path updated, guard test now asserts 4 quarantined files.
2026-07-06 10:41:43 -03:00
Diego Rodrigues de Sa e Souza
5d07bdd76f perf(release-green): run the 4 slow suites concurrently — pre-flight wall time ~sum -> ~slowest (#6319)
The pre-flight ran unit (~25-35min) + vitest (~3-8min) + integration (~3-10min)
+ pack-artifact (~15min) SEQUENTIALLY via execFileSync — ~1h wall in the
v3.8.45 release (the dominant cost of Phase 0). They are independent processes
with per-process DATA_DIR isolation, so they now run in a single Promise.all
over an async runAsync() twin of run(): total drops to ~the slowest single
suite (~30min). Each still saves its per-gate log (_artifacts/release-green/)
and reports HARD independently. --quick still skips them; --with-build folds
pack-artifact into the same wave (or runs it alone under --quick).
Guard: tests/unit/validate-release-green.test.ts (13/13).
2026-07-06 08:25:48 -03:00
Diego Rodrigues de Sa e Souza
ab8b41bdaa docs(i18n): sync finalized [3.8.45] CHANGELOG section into 42 mirrors (parallel-cycle sync-back follow-up) 2026-07-06 04:27:53 -03:00
Diego Rodrigues de Sa e Souza
1f419cf253 chore(release): sync main (v3.8.45 close) into release/v3.8.46 — parallel-cycle sync-back 2026-07-06 04:18:43 -03:00
Diego Rodrigues de Sa e Souza
192f38d58a chore(release): open the v3.8.46 cycle (parallel-cycle model, cut at the v3.8.45 freeze)
Bump package.json x3 + openapi to 3.8.46, add the '## [3.8.46] — TBD'
CHANGELOG section (+42 i18n mirrors), regenerate lockfiles. Development
continues here while the captain closes v3.8.45 (Hard Rule #21);
closing fixes arrive via the Phase 5 sync-back.
2026-07-06 01:03:45 -03:00
Diego Rodrigues de Sa e Souza
264dda77be chore(quality): v3.8.45 cycle-close cognitive/cyclomatic rebaseline (Phase 0 drift absorption)
cognitive 867->877 (+10), cyclomatic 2028->2035 (+7) — inherited cycle drift
measured by check:release-green (hermetic) on the release tip; the captain's
pre-flight fixes are gate/test/workflow changes (complexity-neutral).
Justification keys: _rebaseline_2026_07_06_v3845_release_close.
2026-07-05 23:25:41 -03:00
Diego Rodrigues de Sa e Souza
5ecca12aa5 chore(quality): v3.8.45 cycle-close file-size rebaseline (Phase 0 drift absorption)
13 files grown by the cycle's merged PRs (#6216 streaming+request-logger UI;
#6251/#6253 dashboard UX) — legitimate merged-feature growth absorbed by the
release captain per the Phase 0 drift policy; all entries stay frozen (cannot
grow further). Justification key: _rebaseline_2026_07_06_v3845_release_close.
2026-07-05 22:01:37 -03:00
Diego Rodrigues de Sa e Souza
b74c63a39e ci(vps): hermetic nightly pre-flight on the release runner (descoped: e2e/integration/electron stay hosted) (#6305)
* ci(vps): extend the dynamic omni-release runner to e2e/integration/electron + nightly pre-flight

Completes the #6284 rollout to the jobs where the VPS pays the most:
- test-e2e (9 shards, 15-20min/shard hosted — dominated by setup, not tests),
  test-integration (2 shards) and electron-package-smoke now pick the
  self-hosted omni-release runner under the same gate: vars.USE_VPS_RUNNER
  == 'true' AND own-origin (fork PRs never reach self-hosted).
- nightly-release-green (the release pre-flight) becomes runner-dynamic too:
  on the VPS it runs in a clean env — no operator OMNIROUTE_API_KEY, no
  local noauth CLIs — eliminating the machine-specific false positives that
  dominated the 2026-07-05 pre-flight; passes --hermetic (no-op until the
  #6300 validator lands, then belt-and-suspenders).

Validation plan (per operator request): release-runner-up.sh -> full ci.yml
workflow_dispatch on this branch exercising e2e/integration/electron on the
VPS (proves playwright --with-deps + xvfb on the runner) -> down.sh -> VM
off verified.

* fix(mitm): test suite and CI must never mutate the OS trust store (OMNIROUTE_SKIP_SYSTEM_TRUST)

Incident 2026-07-05 on the self-hosted release runner (VM 113): the
integration test 'POST /cert: installs trust when cert exists' exercised the
REAL install path, wrote a 105-byte fake PEM (FakeMITMCertForTestingOnly)
into /usr/local/share/ca-certificates and update-ca-certificates baked the
invalid entry into ca-certificates.crt — breaking ALL system TLS on the VM
(curl error 77, apt cert failures, and the intermittent gzip-corrupted
next-build artifacts that failed 6/9 e2e shards in run 28754447912). Hosted
runners are ephemeral, so the same mutation went unnoticed for months.

- installCert/uninstallCert: skip the OS dispatch under
  OMNIROUTE_SKIP_SYSTEM_TRUST=1 — AFTER the input checks, so the #4546
  environment-skip contract (missing file throws -> structured skip) and the
  already-installed/not-installed early returns are preserved.
- installTproxyCa/uninstallTproxyCa: same guard, only when no run dep is
  injected (DI'd tests keep exercising the full command sequence with mocks).
- tests/_setup/isolateDataDir.ts sets the env for every node:test process;
  ci.yml/quality.yml/nightly-release-green.yml set it workflow-wide (e2e runs
  the real app outside the test setup).

TDD: tests/unit/system-trust-test-guard.test.ts (guard exported to every test
process; guarded install resolves on a real file without touching the OS;
missing-file contract preserved). 82/82 across the affected cert/tproxy/
agent-bridge suites.

* ci(vps): descope — e2e/integration/electron stay on hosted runners; keep the hermetic nightly pre-flight dynamic

Validation verdict (runs 28754447912 + 28757670732, VM 113):
- e2e cannot run >1 per VM: both jobs bind port 20128 ('already used') — needs
  a per-job port in the playwright runner before any VM rollout.
- concurrent ~1GB artifact downloads truncate on the VM uplink (gzip 'invalid
  compressed data' — with 2 runners the e2e shard passed; corruption returned
  at 4) — actions/download-artifact has no integrity retry here.
- integration shard 2 exceeded its 15-min timeout twice on the VM.
The VPS remains a win for whole-machine jobs: build/unit/vitest (already
dynamic via #6284) and nightly-release-green (single job, clean env, hermetic
pre-flight) — which this PR keeps.
2026-07-05 21:49:48 -03:00
Diego Rodrigues de Sa e Souza
6c1d597d42 fix(mitm): test suite and CI must never mutate the OS trust store (OMNIROUTE_SKIP_SYSTEM_TRUST) (#6310)
Incident 2026-07-05 on the self-hosted release runner (VM 113): the
integration test 'POST /cert: installs trust when cert exists' exercised the
REAL install path, wrote a 105-byte fake PEM (FakeMITMCertForTestingOnly)
into /usr/local/share/ca-certificates and update-ca-certificates baked the
invalid entry into ca-certificates.crt — breaking ALL system TLS on the VM
(curl error 77, apt cert failures, and the intermittent gzip-corrupted
next-build artifacts that failed 6/9 e2e shards in run 28754447912). Hosted
runners are ephemeral, so the same mutation went unnoticed for months.

- installCert/uninstallCert: skip the OS dispatch under
  OMNIROUTE_SKIP_SYSTEM_TRUST=1 — AFTER the input checks, so the #4546
  environment-skip contract (missing file throws -> structured skip) and the
  already-installed/not-installed early returns are preserved.
- installTproxyCa/uninstallTproxyCa: same guard, only when no run dep is
  injected (DI'd tests keep exercising the full command sequence with mocks).
- tests/_setup/isolateDataDir.ts sets the env for every node:test process;
  ci.yml/quality.yml/nightly-release-green.yml set it workflow-wide (e2e runs
  the real app outside the test setup).

TDD: tests/unit/system-trust-test-guard.test.ts (guard exported to every test
process; guarded install resolves on a real file without touching the OS;
missing-file contract preserved). 82/82 across the affected cert/tproxy/
agent-bridge suites.
2026-07-05 21:40:01 -03:00
Diego Rodrigues de Sa e Souza
6f41775a1c fix(security): 405 method-first for /api/keys/{id}/devices (dast-smoke QUERY check)
Schemathesis's newer unsupported-methods check (unpinned tool drift) sends
QUERY /api/keys/{id}/devices and demands 405 Method Not Allowed; the path had
no HIGH_RISK_METHOD_RULES entry, so the auth layer answered 401 first. Add
the devices rule (GET-only) so undocumented methods get a clean method-first
405 — same pattern as the v3.8.44 TRACE fix. TDD:
tests/unit/dast-method-not-allowed.test.ts gains the devices QUERY case (4/4).
2026-07-05 21:19:43 -03:00
Diego Rodrigues de Sa e Souza
ffe825b6b8 fix(quality): clear the 2 remaining heavy-gate reds on the release tip
- check:error-helper: githubSkillTools.ts (#6186 wave) built MCP install
  error results with raw err.message — routed through sanitizeErrorMessage()
  (Hard Rule #12; 19/19 agentSkillTools tests green)
- check:mutation-test-coverage: tests/unit/combo-provider-cooldown-sibling.test.ts
  (#6216) was missing from stryker.conf tap.testFiles — added so its mutant
  kills count on nightly-mutation
2026-07-05 21:08:50 -03:00
Diego Rodrigues de Sa e Souza
dd12539a2c fix(api): Zod-validate POST /api/github-skills + document new gate envs + pin merge-integrity actions
Three latent heavy-CI reds surfaced by the VPS validation dispatch (the fast
path never runs these gates):

- t06 route-validation: POST /api/github-skills destructured request.json()
  blind — a non-array 'targets' would .map-crash. Now validateBody(zod)
  with defaults preserved (Hard Rule #7). Guard:
  tests/unit/github-skills-route-validation.test.ts (4/4).
- env-doc-sync: document OMNIROUTE_SKIP_SYSTEM_TRUST (#6310) and the
  changelog-integrity gate envs CHANGELOG_BASE_REF/ALLOW_CHANGELOG_REMOVALS
  (#6300) in .env.example + ENVIRONMENT.md.
- zizmor ratchet: the new merge-integrity job's checkout/setup-node uses were
  unpinned (+1 finding, 160 > baseline 159); pinned by SHA -> 158 (< baseline).
2026-07-05 20:04:49 -03:00
Diego Rodrigues de Sa e Souza
5c953d1f51 fix(quality): type the 7 net-new 'as any' casts from #6292 (Lint red on the release tip)
#6292 rewrote/added zero-width-marker tests with 7 new 'as any' result casts,
pushing the file past its frozen suppression (84) — and when violations
exceed the suppressed count ESLint reports ALL of them, so the ci.yml Lint
job went red with 90 errors. The 7 new sites get typed shapes (delta /
arguments / choices / output accessors — same pattern as fecf888fd); the
pre-existing debt stays frozen at the exact new count (83). 50/50 tests
green, file lint-clean under the suppressions baseline.
2026-07-05 18:34:45 -03:00
Diego Rodrigues de Sa e Souza
1ad8b3b637 fix(a2a): finish the #6186 catalog-count update — 3 hardcoded 22s left in production
#6186 added omni-github-skills (API 22 -> 23) and updated computeCoverage's
total, but left the old count hardcoded in the A2A layer: listCapabilities
metadata reported coverage.api.total 22 (type literal + value) and
SkillCoverageSchema pinned z.literal(22) — so the schema would REJECT the
correct runtime value. Aligned all three to 23 + the unit fixtures
(listCapabilities-a2a, agentSkills-schemas, 46/46 with agentSkillTools-mcp).
2026-07-05 17:54:41 -03:00
Diego Rodrigues de Sa e Souza
efc92c6955 ci(quality): merge-integrity fast-gates + pre-flight hermetic mode (#6300)
Merged into release/v3.8.45. CI merge-integrity fast-gates (changelog-integrity + agent-skills-sync) + pre-flight hermetic mode. O gate novo detectou 11 SKILL.md gerados fora de sync (drift pré-existente no tip: omni-api-keys endpoints, omni-github-skills do #6186) — regenerados neste PR, Merge-integrity GREEN no CI. Reds restantes base-red (dast-smoke/file-size drift/live-data flaky). Testes novos 17/17.
2026-07-05 17:50:10 -03:00
Denis Kotsyuba
aabefc8026 fix(sse): strip zero-width markers from streamed tool-call arguments (follow-up to #5857) (#6292)
Merged into release/v3.8.45. Strip zero-width markers from streamed tool-call arguments (#5857 follow-up). Test 50/50. CHANGELOG reconciled net +1. Reds pré-existentes (dast-smoke/file-size drift). Thanks @DKotsyuba.
2026-07-05 17:40:22 -03:00
HenryHaniHannoush
8e33393668 fix(docker): add id= to BuildKit cache mounts for strict builders (#6291)
Merged into release/v3.8.45. Dockerfile-only: explicit id= on BuildKit cache mounts (fixes strict-frontend parse error). Reds pré-existentes (dast-smoke/file-size drift) não relacionados a mudança de Dockerfile. Thanks @karimalsalah.
2026-07-05 17:38:52 -03:00
Diego Rodrigues de Sa e Souza
234956ddf5 fix: bug-fix sweep — log path, AgentBridge DNS, opencode-go headers, GitLab Duo tools, M365 EDU (#6197 #6127 #6198 #5997 #6220 #6210) (#6234)
Merged into release/v3.8.45. Bug-fix sweep (#6197 log path, #6127/#6198 AgentBridge DNS, #6210 M365 EDU, #6220 GitLab Duo tools, #5997 opencode-go headers). 27 testes verdes. CHANGELOG restaurado net +5. Reds: dast-smoke + file-size drift.
2026-07-05 17:37:21 -03:00
Diego Rodrigues de Sa e Souza
01ce92a3c0 fix(sse): drop commentary-phase text in Responses passthrough (#6199) (#6232)
Merged into release/v3.8.45. Responses commentary-phase filter (#6199). Sincronizado; CHANGELOG restaurado net +1. Reds: dast-smoke + file-size drift.
2026-07-05 17:35:13 -03:00
Diego Rodrigues de Sa e Souza
ddd546483f fix(resilience): evict sticky affinity on pinned-account failover (#6219) (#6231)
Merged into release/v3.8.45. Sticky affinity failover (#6219). Sincronizado com tip; CHANGELOG restaurado (base bullets preservados, net +1). Reds pré-existentes: dast-smoke + file-size drift.
2026-07-05 17:33:51 -03:00
Diego Rodrigues de Sa e Souza
b6ffe8ce20 fix(proxy): make "Test All" read-only + add bulk enable/disable (#6246) (#6299)
Merged into release/v3.8.45. Delta do par #6246 (Test-All read-only + bulk enable/disable), reconciliado com o núcleo #6296 já mergeado. CHANGELOG restaurado (ambos bullets do #6246 coexistem). Reds pré-existentes: dast-smoke (infra) + file-size drift.
2026-07-05 17:32:05 -03:00
Diego Rodrigues de Sa e Souza
f680aacff0 fix(proxy): #6246 stop the v3.8.44 proxy IP-leak + over-deactivation regression (#6296)
Merged into release/v3.8.45. Reds pré-existentes classificados: dast-smoke (infra), check:file-size (drift de baseline de god-files congelados), e 3 testes de contagem agentSkills stale (43→44 já corrigidos no tip pelo #6186 — somem no squash sobre o tip). Núcleo do fix #6246 (proxy IP-leak + over-deactivation).
2026-07-05 17:26:38 -03:00
Diego Rodrigues de Sa e Souza
dc5ae96905 chore(quality): prune stale ESLint suppressions (4,273 -> 4,233)
Entries whose violations no longer exist (cleaned by cycle merges and the
pre-flight fixes) made 'npm run lint' exit 2 with 'suppressions left that do
not occur anymore'. Regenerated via --prune-suppressions; net-new policy
unchanged.
2026-07-05 16:22:58 -03:00
Diego Rodrigues de Sa e Souza
265d93ffd8 fix(combo): restrict the #6216 empty-stream failover to truly empty bodies (restores #3399/#3685 contracts)
The 'streaming no recognized content' branch added by #6216 marked ANY
stream that ended without content deltas as invalid — sweeping in two
regression-guarded pass-through contracts: an empty stream terminated by an
explicit 'data: [DONE]' (#3399 context-cache protection) and an incomplete
Claude lifecycle (ping only, no message_start; #3685 — stream-readiness
timeout territory, not failover). Both unit guards were red on the branch
and green on main (86/86 vs 84/86).

The branch now fires only for a truly EMPTY body (zero bytes — the Gemini
HTTP-200-empty case that motivated #6216), tracked via sawAnyBytes. New
guard: '#5976 truly EMPTY streaming body (zero bytes) -> invalid for combo
failover'. 87/87 across both files.

Also in this pre-flight batch:
- agentSkillTools-mcp: api.have upper bound 22 -> 23 (the #6186 catalog
  addition updated the totals but missed this bound)
- delete tests/unit/free-provider-rankings-configured-filter.test.ts:
  #6251 (server-side configuredOnly/availableOnly) superseded the #6245
  client-side toggle it pinned; replacement declared in the test-masking
  allowlist (tests/unit/freeProviderRankings-filters.test.ts, 11/11)
2026-07-05 16:07:32 -03:00
Diego Rodrigues de Sa e Souza
bf1481f11f fix(skills): generate the missing omni-github-skills registry entry + align catalog count tests
PR #6186 added omni-github-skills to the agent-skills catalog (API 22 -> 23)
but did not run the generator, so skills/omni-github-skills/SKILL.md never
existed and 6 integration assertions split between the old (42/43) and new
counts. Generated via scripts/skills/generate-agent-skills.mjs --apply and
aligned agent-skills-discovery to the real totals (43 = 23 API + 20 CLI;
handlers return 44 with config-codex-cli). 30/30 discovery+content tests green.
2026-07-05 14:58:40 -03:00
Diego Rodrigues de Sa e Souza
fecf888fd9 fix(quality): clear the cycle's 11 net-new ESLint errors + make validate-release-green suppressions-aware
- executor-kiro/save-call-log/call-logs-correlation tests: replace 15 'as any'
  casts with typed shapes (net-new no-explicit-any errors from #6213/#6216);
  prune the now-empty suppression entries so the frozen baseline stays exact
- github-skills + usage/call-logs routes: raw toLowerCase().includes() search
  replaced by matchesSearch() (no-restricted-syntax — Turkish-safe search,
  behavior covered by tests/unit/call-logs-correlation-substring.test.ts and
  tests/unit/github-collector.test.ts)
- validate-release-green.mjs: run ESLint with --suppressions-location (match
  the npm run lint contract — frozen debt is not a release red) and raise the
  lint timeout 15->30min (a full pass takes ~14min alone; the 15min ceiling
  expired under concurrent suite load and surfaced as 'could not parse eslint
  json')
2026-07-05 14:49:18 -03:00
Diego Rodrigues de Sa e Souza
509fd5425c chore(release-green): clear test-masking + docs-all HARD reds for the v3.8.45 pre-flight
- test-masking: allowlist the 4 verified-legitimate assert reductions of the
  cycle (#6248 MiMo V2 removal, #6170 Kiro catalog correction, #6154 Copilot
  catalog refresh) and register the #6164 AutoRoutingBanner test deletion with
  a real replacement guard (tests/unit/home-no-autorouting-banner.test.ts —
  asserts the banner stays out of home/page.tsx and the component stays deleted)
- docs-sync: executors count 68 -> 73 in ARCHITECTURE.md + CODEBASE_DOCUMENTATION.md
- env-doc-sync: document OMNIROUTE_NO_SUDO (#6249/#6122) in .env.example +
  docs/reference/ENVIRONMENT.md
2026-07-05 14:33:54 -03:00
Diego Rodrigues de Sa e Souza
faf68a222f fix(dashboard): null-guard connection in EditConnectionModal base-URL override (#6147) (#6287)
fix(dashboard): null-guard connection in EditConnectionModal (#6147) — fixes 'Cannot read properties of null (reading authType)' crash on every provider-detail page entry. TDD: connModals.test.tsx null-mount 9/9. Base-reds only. Integrated into release/v3.8.45.
2026-07-05 12:08:22 -03:00
Diego Rodrigues de Sa e Souza
8a2b522a53 docs(changelog): restore v3.8.45/v3.8.44 sections eaten by the #6193 merge auto-resolve + CI-perf campaign bullets (#6273 #6275 #6283 #6284 #6285) 2026-07-05 11:37:37 -03:00
Diego Rodrigues de Sa e Souza
bfd8a6533f ci: opt-in self-hosted VPS runners for the release window (anti-queue) (#6284)
Adds the on-demand self-hosted runner plumbing for /generate-release:

- scripts/vps/release-runner-up.sh: starts the runner VM on Proxmox, waits
  for >=1 'omni-release' runner to report online via the GitHub API, then
  flips the USE_VPS_RUNNER repo variable to true. Any failure/timeout sets
  it back to false and exits 1 so the caller falls back to hosted runners.
- scripts/vps/release-runner-down.sh: flips USE_VPS_RUNNER=false FIRST
  (so no job gets scheduled onto a dying runner), then gracefully shuts
  the VM down. Idempotent.
- ci.yml: build, test-unit x8 and test-vitest pick their runner
  dynamically. Self-hosted is used ONLY when USE_VPS_RUNNER == 'true'
  AND the event is own-origin (push/dispatch, or a PR whose head repo is
  this repository). Fork PRs and the var's default/absent state always
  fall back to ubuntu-latest.

Why: the Free plan caps hosted concurrency at 20 jobs; a release run
saturates it and queues. The benchmarked VPS (32-core, 4 runners) matches
hosted per-job times (unit ~8.5min/shard, build 9min turbo) but eliminates
the queue, which is the real release bottleneck (~30-50min on a busy day).
Scope is conservative: only the three job families benchmarked on the VPS;
e2e/electron/integration stay hosted (playwright/xvfb provisioning not
validated on the runner workspace yet).
2026-07-05 11:14:30 -03:00
Diego Rodrigues de Sa e Souza
c26984e9bd feat(docker): build the image with Turbopack (v3.8.27 panic gone on Next 16.2.9) (#6285)
Flips the builder stage to OMNIROUTE_USE_TURBOPACK=1 and rewrites the stale
panic comment: the v3.8.27-era TurbopackInternalError ('entered unreachable
code: there must be a path to a root' in ImportTracer::get_traces) no longer
reproduces on Next 16.2.9.

Validation (2026-07-05, this exact Dockerfile, default
OMNIROUTE_BUILD_MEMORY_MB=4096, no overrides):
- amd64: docker build 659s, exit 0, zero panic/OOM strings in the full log,
  container smoke-tested (/api/monitoring/health 200)
- arm64 (qemu): exit 0, zero panic strings

Webpack stays available as the escape hatch:
--build-arg / -e OMNIROUTE_USE_TURBOPACK=0. The V8 heap ceiling is kept:
Turbopack's compile is native Rust, but prerender/export still runs on V8.
2026-07-05 11:14:27 -03:00
Diego Rodrigues de Sa e Souza
046093b9cb feat(build): make Turbopack the default bundler for dev and build (#6283)
Turbopack (stable in Next 16) becomes the code default in the three entry
points that previously required an explicit OMNIROUTE_USE_TURBOPACK=1:

- scripts/build/build-next-isolated.mjs (production build)
- scripts/dev/run-next.mjs (dev server)
- scripts/dev/run-next-playwright.mjs (playwright dev runner)

OMNIROUTE_USE_TURBOPACK=0 remains the webpack escape hatch (Windows /
native-binding / bundler-compat issues), and only the documented '0'
opts out — junk values keep the default.

Benchmarked on this codebase (same tree, Next 16.2.9): webpack 1035s vs
Turbopack 539s on a 32-core box; ~20min vs 6min59s on ubuntu-latest.
Artifact validated end-to-end (standalone smoke + e2e/package-artifact/
electron-package-smoke CI jobs, Docker amd64+arm64 builds clean with the
v3.8.27 ImportTracer panic gone on 16.2.9).

TDD: tests/unit/build-bundler-default-turbopack.test.ts (new) +
run-next-playwright.test.ts extended with the unset-env default case;
both red before the flip, green after. ENVIRONMENT.md updated.
2026-07-05 11:14:25 -03:00
Diego Rodrigues de Sa e Souza
f35839fe01 ci(build): switch Next.js production build to Turbopack (1.9x faster) (#6273)
Next 16 ships Turbopack as the stable production bundler. Benchmarked on a
32-core box against the same tree: webpack 1035s -> Turbopack 539s (1.92x).
The build script already supports the switch via OMNIROUTE_USE_TURBOPACK=1
(scripts/build/build-next-isolated.mjs) and next.config.mjs already mirrors
the resolveAlias stubs in the turbopack block, so this only flips the CI env.

The webpack .build/next/cache actions/cache step is removed in the same
commit: Turbopack does not use the webpack cache dir (its persistent FS
cache is experimental and NOT enabled), so restoring ~0.5 GB per run would
be pure wasted download. Revert restores webpack + its cache together.

Validation: standalone output smoke-tested (server.js boots, health 200,
dashboard 307); the 428 build warnings are the known benign 'overly broad
file pattern' static-analysis notices for dynamic fs usage (covered at
runtime by outputFileTracingIncludes). Downstream e2e x9, package-artifact
and electron-package-smoke consume this artifact, so a green CI run here
validates the Turbopack artifact end-to-end. nightly-compat and npm-publish
stay on webpack until this PR proves out.
2026-07-05 11:14:16 -03:00
Diego Rodrigues de Sa e Souza
143b7b13c4 ci: unblock test jobs from the Build gate (start at minute 0) (#6275)
test-unit x8, test-vitest, test-integration x2 and test-security all had
needs: build but never download the next-build artifact — the dependency
only serialized ~20min of Build wall-clock in front of every test run.
Switch them to needs: changes with the same skip condition Build uses
(docs-only PRs and drafts still skip), so the test chain
(test-unit -> test-coverage -> quality-gate -> sonarqube) now runs in
parallel with Build instead of after it.

Jobs that genuinely consume the artifact keep needs: build unchanged:
test-e2e x9, package-artifact, electron-package-smoke.

Expected effect on a full ci.yml run: critical path drops from
build + tests (~30min+) to max(build, tests) — roughly 15-20min saved
per run, no extra runner minutes beyond starting the same jobs earlier.
2026-07-05 11:14:14 -03:00
Diego Rodrigues de Sa e Souza
fc16dcd6ee feat(dashboard): filter Free Provider Rankings by configured/available (#6150) (#6251)
feat(dashboard): configured-only / available-only filters on Free Provider Rankings (#6150) — server-side query params + tested lib helper; supersedes the client-side #6245 toggle with an available-only dimension. Lib logic 11/11 green; UI validated live on VPS. Base-reds only. Integrated into release/v3.8.45.
2026-07-05 10:52:14 -03:00
Markus Hartung
f237c07def Fix/5976 continued (#6216)
fix(combo): 5 streaming-path fixes (#5976) — locked-stream 500, error-frame-only-if-no-content, Gemini MALFORMED_RESPONSE→content_filter failover, correlationId substring, per-model-500 lockout skip + request-logger UI. Merged — thank you, @hartmark! Maintainer follow-up: releaseQualityClone cancels the abandoned quality-check tee branch (per-request memory) + regression test. All 41 combo/streaming quality tests green. Base-reds only. Integrated into release/v3.8.45.
2026-07-05 08:37:49 -03:00
Moseyuh333
9899a6dacc fix(github-skills): add missing import, add unit tests, fix settings JSON parse (#6186)
feat(skills): GitHub skill-discovery subsystem — search/score/scan/import agent skills from GitHub, MCP tools (scope-gated) + /api/github-skills route (host-pinned api.github.com, sanitized errors). Merged — thank you, @Moseyuh333! Pre-merge fixes: passed toolDef.scopes so tools gate correctly under scope enforcement, routed the GET error path through sanitizeErrorMessage+500 (Hard Rule #12), and realigned the agent-skills count guards (catalog/routes/generator/mcp) for the new catalog entry. Base-reds only. Integrated into release/v3.8.45.
2026-07-05 08:28:01 -03:00
backryun
4d330dafa9 fix(providers): remove deprecated MiMo v2 entries (#6248)
chore(providers): remove deprecated MiMo V2 catalog entries (superseded by V2.5); realign provider-catalog tests. Merged — thank you, @backryun! Base-reds only. Integrated into release/v3.8.45.
2026-07-05 07:54:57 -03:00
Diego Rodrigues de Sa e Souza
9285dc1b89 feat(providers): add Requesty as an OpenAI-compatible gateway provider (#6120) (#6250)
feat(providers): add Requesty as an OpenAI-compatible gateway provider (#6120). Fixed the APIKEY_PROVIDERS count guard (167→168) the feature had missed. TDD guard requesty-provider.test.ts (4/4) + providers-constants-split (4/4). Base-reds only. Integrated into release/v3.8.45. (thanks @chirag127)
2026-07-05 07:51:33 -03:00
Diego Rodrigues de Sa e Souza
149b086d0f feat(docker): OMNIROUTE_NO_SUDO env flag for root-less MITM cert trust (#6122) (#6249)
feat(docker): OMNIROUTE_NO_SUDO env flag for root-less MITM cert trust (#6122). resolveSudoSpawn strips sudo when set; argv-array spawn preserved (Hard Rule #13). TDD guard mitm-systemCommands-no-sudo.test.ts (5/5). Base-reds only. Integrated into release/v3.8.45. (thanks @powellnorma)
2026-07-05 07:48:47 -03:00
Diego Rodrigues de Sa e Souza
1044821bda feat(combo): add option to disable session stickiness (#6168) (#6252)
feat(combo): add option to disable session stickiness (#6168) — per-combo/global, precedence config→settings→false (preserves #3825). TDD guard combo-disable-session-stickiness.test.ts (8/8). Base-reds only. Integrated into release/v3.8.45. (thanks @RCrushMe)
2026-07-05 07:47:34 -03:00
Diego Rodrigues de Sa e Souza
cefbcfb278 feat(dashboard): routing/settings UX clarity — share %, Cloud Sync rename, base-URL override (#6147) (#6253)
feat(dashboard): routing/settings UX clarity (#6147) — effective share %, Cloud Sync→Remote Settings Sync rename, opt-in advanced base-URL override. TDD guard routing-settings-ux-6147.test.ts (6/6). Base-reds only. Integrated into release/v3.8.45.
2026-07-05 07:46:18 -03:00
Diego Rodrigues de Sa e Souza
776a7a3a58 feat(providers): bulk-add API keys for Cloudflare Workers AI (#6174) (#6254)
feat(providers): bulk-add API keys for Cloudflare Workers AI (#6174). Per-entry providerSpecificData (fixes shared-object reuse); TDD guard bulk-api-key-parser-cloudflare.test.ts. Base-reds only. Integrated into release/v3.8.45. (thanks @muflifadla38)
2026-07-05 07:42:29 -03:00
Diego Rodrigues de Sa e Souza
5531fc7f05 feat(providers): route built-in agentrouter through dynamic CC wire image (#6056) (#6255)
feat(providers): route built-in agentrouter through dynamic CC wire image (#6056). TDD-covered (agentrouter-cc-wire-image.test.ts). Base-reds only. Integrated into release/v3.8.45.
2026-07-05 07:41:15 -03:00
Diego Rodrigues de Sa e Souza
826a66f287 feat(providers): add Yuanbao (web) cookie-session provider (#6196) (#6256)
feat(providers): add Yuanbao (web) cookie-session provider (#6196). TDD-covered; base-reds only (dast-smoke #6228, docs version-drift, executor-kiro anys — #6145 guard fixed on tip via #6270). Integrated into release/v3.8.45.
2026-07-05 07:37:17 -03:00
Diego Rodrigues de Sa e Souza
58abebd106 test(dashboard): realign #6145 onboarding-href guard to the #6166 helper refactor (#6270)
Realign the #6145 onboarding-href guard to the #6166 helper refactor (buildProviderDetailsHref). Test-only; unblocks the fast-path unit job across the open PR queue. Base-reds only (dast-smoke #6228, docs version-drift, executor-kiro anys). Integrated into release/v3.8.45.
2026-07-05 07:34:49 -03:00
serverless83
c347abb774 fix(i18n): add 118 missing Italian translations (#6212)
i18n(it): add 118 Italian translations (#6212). Audited net-additive (0 keys dropped, valid JSON). Thanks @serverless83. Integrated into release/v3.8.45.
2026-07-05 05:48:35 -03:00
Milan Soni
f26aa16da3 feat(rankings): add 'Configured Only' filter to Free Provider Rankings page (#6245)
feat(rankings): add 'Configured Only' filter to Free Provider Rankings (#6245, closes #6150). 9/9 test green. Thanks @Iammilansoni. Integrated into release/v3.8.45.
2026-07-05 05:46:17 -03:00
KooshaPari
0e0ca7e26b fix(dashboard): use connection.id (UUID) not connection.provider (category) in onboarding wizard href (issue #6144) (#6166)
refactor(dashboard): extract tested buildProviderDetailsHref helper for onboarding wizard (#6166). Behavioral #6144 fix already on tip via #6145; this lands the tested-helper hardening. Thanks @KooshaPari. Integrated into release/v3.8.45.
2026-07-05 05:37:42 -03:00
Vittor Guilherme Borges de Oliveira
7e2b839935 fix(security): require management auth for mutable cloud routes (#6233) (#6233)
fix(security): require management auth for mutable cloud routes (#6233). Verified: 3 PR tests + full authz/route-guard suite 241/241 green. Thanks @vittoroliveira-dev. Integrated into release/v3.8.45.
2026-07-05 05:33:19 -03:00
Diego Rodrigues de Sa e Souza
9ebb53e432 fix(antigravity): strip trailing assistant prefill turn for Vertex Claude models (#6114)
fix(antigravity): strip trailing assistant prefill for Vertex Claude models (#6114). TDD-covered (6/6), merged on TDD strength per owner. Reds are pre-existing base-red drift on release/v3.8.45. Thanks @anki1kr. Integrated into release/v3.8.45.
2026-07-05 04:21:38 -03:00
Diego Rodrigues de Sa e Souza
9827ae6137 fix(api): count tool_use/tool_result/thinking blocks in count_tokens estimate (port from 9router#2337) (#6221)
fix(api): count tool_use/tool_result/thinking blocks in count_tokens estimate (#6221, port from 9router#2337). TDD-covered (6/6); typed the test casts to clear no-new-eslint. Reds are pre-existing base-red drift on release/v3.8.45 (dast-smoke #6228; executor-kiro.test.ts eslint anys; changelog/package.json version drift) — none introduced by this PR. Thanks @luweiCN. Integrated into release/v3.8.45.
2026-07-05 04:20:54 -03:00
Diego Rodrigues de Sa e Souza
9b986fa220 docs(architecture): sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, db-schema diagram and llm.txt (+42 i18n mirrors) (#6167)
docs(architecture): sync stale DB-layer counts (45+/55 → 95+/110+) across REPOSITORY_MAP, db-schema diagram, llm.txt + 42 i18n mirrors (#6167). Docs-only; check:docs-all passes locally on the reconstruction. Reds are pre-existing base-red drift on release/v3.8.45 (dast-smoke #6228; executor-kiro.test.ts eslint anys; changelog/package.json version drift) — none introduced by this PR. Integrated into release/v3.8.45.
2026-07-05 04:18:56 -03:00
Rian Priskanova
05857018f4 fix(mitm): strip colons from macOS cert fingerprint before keychain match (#6134) (#6204)
fix(mitm): strip colons from macOS cert fingerprint before keychain match (#6134). Extracted testable macCertOutputHasFingerprint helper + regression guard. Thanks @rianonehub. Integrated into release/v3.8.45.
2026-07-05 04:18:41 -03:00
Diego Rodrigues de Sa e Souza
5acfbe882f fix(oauth): align zed in OAUTH_PROVIDER_IDS + config enum after #6078 merge
#6078 registered zed in the OAuth PROVIDERS registry but did not add it to the
constants PROVIDERS id map nor the oauth-providers-config enumeration test, leaving
that test red on the release tip (getProvider enumeration vs EXPECTED mismatch).
Adds ZED to the id constants + zed to EXPECTED_PROVIDER_KEYS/EXPECTED_CONFIG_BY_PROVIDER.
2026-07-05 03:34:45 -03:00
Ankit
b834c740d1 fix(providers): register zed in OAuth PROVIDERS to fix Unknown provider error (#6041) (#6078)
Registers a minimal import_token entry for the existing Zed IDE keychain-import
provider so getProvider("zed") no longer throws "Unknown provider: zed" when the
UI probes the OAuth capability endpoint; generateAuthData returns { supported: false }.

Test runner fix: the regression test imported from "vitest" but lives in tests/unit/
(node:test territory, outside the vitest include globs) — it ran in no runner. Converted
to node:test + node:assert so it actually executes (8/8 green).

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-07-05 03:24:32 -03:00
backryun
44e85a7d40 fix(doubao-web): switch provider to Dola global (#6235)
* fix(doubao-web): switch provider to Dola global

* fix(doubao-web): use .dola.com cookie domain for s_v_web_id + rebaseline test

The Dola switch left s_v_web_id with a host-only "www.dola.com" domain, which fails
the token-source contract (domain must start with "." or "http") — the sibling
sessionid/ttwid cookies and the canonical cookieDomain already use ".dola.com", which
also matches www.dola.com. Also rebaselines web-cookie-providers-new.test.ts (850->890)
for the provider-switch regression cases.

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

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
2026-07-05 02:45:33 -03:00
Aris
74546ed854 fix(doctor): resolve two false-positive WARNs (#6162) (#6163)
* fix(doctor): resolve two false-positive WARNs (#6162)

The `omniroute doctor` command reported two warnings on healthy installs
even though the underlying checks actually passed. Both came from the
doctor probing state that already worked; they looked like bugs but users
couldn't tell without manual digging.

Issue 1 — Server liveness HTTP 401
  /api/health and /api/health/degradation both require the management
  token. Doctor called them without auth → 401 → WARN, even when the
  Next.js server was clearly alive and listening.

  Fix: probe the configured health endpoint first; on 401/403, fall
  back to a publicly served static asset (/favicon.ico) to confirm the
  server is alive. WARN now only fires when both probes fail.

Issue 2 — CLI Tools '@/shared' import
  tool-detector.ts (and 3 other cli-helper files) import @/shared/...
  aliases that resolve via tsconfig.json paths. The CLI ships raw TS
  source (no compile step) and runs through tsx, but tsx does not honor
  tsconfig paths at runtime, and tsconfig-paths only hooks CJS
  Module._resolveFilename while doctor uses ESM `import()`.

  Fix: replace @/shared/... with relative imports in the 4 cli-helper
  files. This is the same pattern these files already use for ./config-
  generator/* imports. No new dependency, no architectural change, and
  the fix doesn't regress Next.js itself which keeps using @/shared.

Verified on v3.8.43 (Node v24.17, Windows 11):
  Before: 7 ok, 2 warning(s), 0 failure(s)
  After:  8 ok, N warning(s), 0 failure(s)
    where N accurately reflects which CLI tools are installed and
    configured for OmniRoute (e.g. Hermes Agent installed but not
    pointed at 20128 → 2 real warnings, not 1 false-positive).

Refs #6162

* fix(doctor): derive fallback URL from primary URL via new URL()

Per Gemini code-assist review feedback: the previous fallback constructed
the /favicon.ico URL from defaults (127.0.0.1:PORT) which ignored custom
host/port/protocol configurations supplied via:
  - OMNIROUTE_DOCTOR_LIVENESS_URL
  - OMNIROUTE_DOCTOR_HOST
  - --liveness-url / --host CLI flags

Parse the primary URL with new URL() to preserve protocol, host, port, and
subpaths. The previous default-based fallback remains as a catch-all for
invalid primary URLs.

* test(doctor): add regression tests for #6162 fixes

Two new test files lock the fix and satisfy the PR Test Policy gate
("production code change without tests"):

- tests/unit/cli-helper-tool-detector-paths-6162.test.ts
    Locks the @/shared → relative imports fix across all 4 cli-helper
    files. Asserts (a) no @/shared alias remains in the cli-helper
    sources, and (b) each file is importable at runtime via tsx/ESM,
    which would have thrown "Cannot find package '@/shared'" before
    the fix.

- tests/unit/cli-doctor-liveness-fallback-6162.test.ts
    Locks the /favicon.ico fallback in doctor.mjs. Asserts the
    fallback probe exists, derives its URL from the primary URL via
    new URL() (per Gemini review feedback), and that the buggy
    'Server responded with HTTP 401' WARN path is gone.

Both tests use only node:test + node:assert/strict so they slot into
the existing 'test' and 'test:unit' scripts with no extra config.

* test(doctor): fix primary.ok regex in fallback test

The earlier regex /primary\.ok\s*\?/ required a '?' immediately after,
but the actual doctor.mjs code uses a multi-line if-block:

  if (primary.ok) {
    return ok(...);
  }

Use /\bprimary\.ok\b/ instead so the assertion matches the existing
branching.

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
2026-07-05 02:41:07 -03:00
Diego Rodrigues de Sa e Souza
00c74ea6c4 chore(quality): rebaseline kiro-translator file-size debt from #6213
The #6213 kiro adaptive-thinking feature grew openai-to-kiro.ts (853->890) and its
test (1093->1234); the fast-path PR->release does not gate check:file-size on merge,
so the growth accumulated on the release tip. Rebaselined to keep the tip green.
Justification recorded in file-size-baseline.json.
2026-07-05 02:38:25 -03:00
backryun
e755c5ac68 fix(providers): refresh GitHub Copilot catalog (#6154)
* fix(providers): refresh github copilot catalog

Limit GitHub Copilot discovery to the curated supported model set and keep the provider cooldown panel client-safe by moving countdown formatting out of localDb.

* chore(quality): rebaseline providerPageHelpers.ts file-size (+13, #6154 copilot catalog)

The GitHub Copilot catalog refresh grows the provider-page model-section helper
(1021->1034). Fast-path PR->release skips check:file-size, so the bump lands with
the PR. Justification recorded in file-size-baseline.json.

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

---------

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-07-05 02:33:32 -03:00
Denis Kotsyuba
b1e27258c0 fix(chatcore): exempt opencode client from the default 128-tool truncation (#6193)
* fix(chatcore): exempt opencode client from the default 128-tool truncation

The default MAX_TOOLS_LIMIT (128) cap made truncateToolList blind-slice
tools.slice(0, 128), dropping opencode's built-in task tool and part of
its MCP tools when the inbound list exceeded 128 — so models routed
through OmniRoute could not launch subagents or reach all their tools.

Detect the opencode client (any x-opencode-* header, or 'opencode' in
the user-agent) and bypass ONLY the speculative 128 default. A known
provider ceiling (proactive PROVIDER_TOOL_LIMITS or a detected limit)
always wins and still truncates, even for opencode, so upstreams with
real hard limits (e.g. grok-cli 200) keep their 400-avoidance guard.
Non-opencode clients are unchanged.

- requestFormat.ts: add isOpencodeClient(headers, userAgent) + expose it
  on resolveChatCoreRequestFormat.
- toolLimitDetector.ts: add getKnownToolLimit(); getEffectiveToolLimit
  becomes getKnownToolLimit(provider) ?? DEFAULT_LIMIT (byte-identical
  for existing callers).
- upstreamBody.ts: truncateToolList takes bypassDefaultToolLimit and
  encodes the precedence; fix cosmetic debug-log count.
- chatCore.ts: thread the flag into prepareUpstreamBody.
- tests: extend tool-limit-detector unit tests.

* refactor(tools): accept nullable provider in tool-limit resolvers

Address PR review: widen getKnownToolLimit / getEffectiveToolLimit to
(provider: string | null | undefined) to match the call sites in
truncateToolList, and add unit assertions covering null/undefined
providers (getKnownToolLimit -> null, getEffectiveToolLimit -> 128).

---------

Co-authored-by: DKotsyuba <16292493+DKotsyuba@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
2026-07-05 02:30:30 -03:00
VXNCXNX
dc7eeba717 feat(sse): surface Kiro adaptive-thinking reasoning as reasoning_content (#6213)
Kiro/CodeWhisperer streams Claude's reasoning as native `reasoningContentEvent`
frames when adaptive thinking is enabled, but the Kiro executor had no handler
for them, so `reasoning_effort` requests returned no reasoning. Wire it end to
end:

- translator (openai-to-kiro): enable Kiro thinking when the request carries
  `reasoning_effort`, Anthropic `output_config.effort`, or a `thinking` block
  (`{type:"enabled",budget_tokens}` mapped to a level; `{type:"adaptive"}`
  defaults to `high`, matching Anthropic's documented default). Prepends the
  Kiro `<thinking_mode>`/`<max_thinking_length>` prompt directive and sets
  top-level `additionalModelRequestFields` ({output_config.effort,
  thinking:{type:"adaptive"}, max_tokens}). Gated on `supportsReasoning`; drops
  non-default temperature/top_p (rejected by adaptive-only Claude models).
- executor transformRequest: forward `additionalModelRequestFields` to AWS
  (previously dropped by the strict top-level allowlist).
- executor stream loop: parse `reasoningContentEvent` (and reasoningText
  variants) into the OpenAI reasoning_content channel.

Verified against the live CodeWhisperer stream: reasoningContentEvent frames are
returned, and larger effort/budget measurably deepens reasoning up to the model
cap. Unit tests cover the effort sources, forwarding, temp/top_p stripping, and
native reasoning-frame parsing.
2026-07-05 02:30:21 -03:00
R. Beltran
b074c6d75e fix(cli): detect POSIX auto-set HOSTNAME via os.hostname() to fix bind address (#6194) (#6195)
POSIX shells (bash/zsh) always set HOSTNAME to the machine name. The
.env loader uses first-wins semantics, so HOSTNAME=0.0.0.0 in .env is
silently ignored. This causes the server to bind to the LAN hostname
instead of 0.0.0.0, breaking localhost access and all internal
self-requests (ModelSync, HealthCheck, cloud sync).

The fix compares process.env.HOSTNAME against os.hostname(): when they
match, it's the POSIX auto-set signature and HOSTNAME is ignored.
OMNIROUTE_SERVER_HOST takes precedence as the dedicated escape hatch.

Backward compatibility is preserved: users who set HOSTNAME to a value
that doesn't match the machine name (e.g. Windows CMD/PowerShell users
with HOSTNAME in .env) will still have their value honoured.

Closes #6194
2026-07-05 02:30:16 -03:00
Milan Soni
5e3a95be4f feat(provider): add Claude 5 Sonnet to Claude Web provider (#6200) (#6209)
* feat(provider): add Claude 5 Sonnet to Claude Web provider (#6200)

* test(providers): guard claude-web claude-sonnet-5 registry entry (#6209)

Adds the missing regression test the PR-test-policy gate requires: asserts the
claude-web registry exposes claude-sonnet-5 (Claude 5 Sonnet web) alongside the
existing 4.6 Sonnet / 4.5 Haiku entries. Fails on the release base (no entry).

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

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
2026-07-05 02:30:10 -03:00
Luis Alejandro Vega
0ed6780798 fix: add nvidia to PROVIDER_TOOL_LIMITS (1536) to prevent tool truncation (#6177)
NVIDIA NIM API (nvidia/* models) silently truncates the tool list to 128
(the default MAX_TOOLS_LIMIT) because nvidia is not in PROVIDER_TOOL_LIMITS.
Tools beyond index 127 are dropped, causing agents to lose access to
critical tools like task, read, or high-index MCP tools.

Verified that NVIDIA NIM API supports up to 1536 tools by direct testing.
End-to-end confirmed: 198 tools sent, model successfully called tools at
indices 193, 195, and 197 (previously dropped by truncation to 128).

Follows the same pattern as #5563 (grok-cli: 200), integrated in v3.8.43.
2026-07-05 02:30:04 -03:00
Danny S
6a12ba07b1 fix(translator): strip reasoning param for nvidia z-ai/glm-5.2 (#6181)
* fix(translator): strip reasoning param for nvidia z-ai/glm-5.2

NVIDIA NIM OpenAI-compatible wrapper rejects the reasoning body field
and returns HTTP 400 "Unsupported parameter(s): `reasoning`".
Add a StripRule scoped to provider=nvidia + model /z-ai\/glm-5\.2/i.
Mirrors PR #6102 drop pattern (minimax-m2.7 thinking).

* docs(translator): tighten nvidia glm-5.2 strip-rule comment

* fix(translator): anchor glm-5.2 strip rule with word boundary
2026-07-05 02:29:59 -03:00
Diego Rodrigues de Sa e Souza
6816bcdaf3 fix(dashboard): providers page data-timeout guard + live-ws standalone wiring (#6211)
* fix(dashboard): providers page data-timeout guard + live-ws standalone wiring

Captura de trabalho em progresso: timeout de dados na página de providers,
ajuste em ProviderLimits e instrumentation-node, com testes novos
(providers-page-data-timeout, live-ws-standalone-wiring).

* chore(quality): rebaseline ProviderLimits/index.tsx file-size (+6, #6211 data-timeout guard)

Cohesive fix growth from PR #6211's data-timeout guard on the quota page's two
first-paint fetches (1121->1127). The fast-path PR->release skips check:file-size,
so the bump lands with the PR. Justification recorded in file-size-baseline.json.
2026-07-05 02:29:40 -03:00
Diego Rodrigues de Sa e Souza
286fdf8794 fix(sse): surface ChatGPT-web image silent-drop as an accurate error (#6208)
When ChatGPT Web generates an image as an image_asset_pointer but the pointer
fails to resolve to a downloadable URL (unknown asset scheme, download 403/
expired, oversize), resolveImagePointers returned [] — indistinguishable from
'no image produced' — so the image-generation handler reported the misleading
502 'completed without returning image markdown'. The image genuinely existed
upstream; OmniRoute dropped it silently.

Fix: the executor flags x_image_resolution_failed when a pointer existed but
none resolved (and logs the unresolved asset scheme for follow-up), and the
handler surfaces a truthful 'generated but not retrievable' 502 instead of
'no image markdown'. Adds executorFactory DI for unit testing.

TDD: tests/unit/chatgpt-web-image-silentdrop.test.ts (red -> green), plus the
existing chatgpt-web / image-generation-handler suites stay green.

Reported via community triage (mesh escalated backlog).
2026-07-05 02:29:37 -03:00
Diego Rodrigues de Sa e Souza
b3a2cfe0ea fix(providers): correct Kiro model catalog to real upstream ids (#6170)
* fix(providers): correct Kiro model catalog to real upstream ids

Kiro's API (generateAssistantResponse) returns 400 "Invalid model. Please
select a different model" for any id it does not recognize. The registry
exposed fabricated ids (copied from OmniRoute's own Anthropic catalog) that
Kiro never serves, so every call to them 400'd. Live-verified on the VPS:

  Removed (400 Invalid model):
    - auto-kiro       (no "auto" model id — was sent verbatim upstream)
    - claude-fable-5  (Kiro offers no Fable)
    - claude-opus-4.8/4.7/4.6 (Kiro offers no Opus)
  Corrected:
    - claude-sonnet-4.6 -> claude-sonnet-4.5 (Kiro's Sonnet is 4.5; 4.5 -> 200)
  Kept:
    - claude-sonnet-5 (real Kiro model, plan-gated per account)
    - claude-haiku-4.5, deepseek-3.2, glm-5, minimax-m2.5/m2.1,
      qwen3-coder-next (all proven 200 on the VPS)

Aligns the free-model catalog and drops the orphaned auto-kiro price key.
Regression guard: tests/unit/kiro-catalog-real-models.test.ts (3/3).
Kiro cluster #6112/#6113/#6099.

* test(providers): align stale Kiro-catalog tests to the corrected upstream ids

The fabricated Kiro ids removed in the parent commit (claude-fable-5,
claude-opus-4.8/4.7/4.6, claude-sonnet-4.6) were still asserted as present by
three pre-existing tests, which encoded the bug:
- catalog-updates-v3x: now asserts Kiro does NOT expose Fable 5 / Opus (kept the
  legit cc exposure) and guards the real claude-sonnet-4.5 pricing.
- model-family-fallback-notation: the dot-notation example moves from kiro/ to
  anthropic/ (which genuinely serves Opus/Fable in dot notation) — coverage kept.
- provider-models-route: the Kiro local-catalog assertion now expects the real
  Sonnet 5 / Sonnet 4.5 set and negatively guards the fabricated ids.

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-07-05 02:29:32 -03:00
Diego Rodrigues de Sa e Souza
f2ad9b23bd fix(cline): force upstream streaming for Cline/ClinePass (streaming-only API) (#6165)
* fix(cline): force upstream streaming for Cline/ClinePass (streaming-only API)

Cline's API (api.cline.bot) only implements streaming (streamText). A
non-streaming request returns HTTP 500 "generateText is not implemented" (Claude
models) or HTTP 502 "empty response" (others). Live-verified on the VPS:
stream:true → works (STREAM_OK), stream:false → fails. This is why testing a Cline
model in the dashboard (the test button sends stream:false) failed.

Fix (reuses the existing isClaudeCodeCompatible mechanism, no new handler):
- Flag `cline` and `clinepass` registry entries with `forceStream: true`.
- In chatCore, OR `providerRequiresStreaming` into `upstreamStream` (line 1591)
  so the upstream request always streams for these providers, while the client's
  original `stream` intent still drives the response format. The existing
  non-streaming branch (parseNonStreamingResponseBody) already accumulates the
  upstream SSE and converts it back to JSON for stream:false clients — the same
  path Claude-Code-compatible providers already use.

Tests (Rule #18): tests/unit/cline-force-stream.test.ts pins the registry flags +
resolveStreamFlag forcing behavior. Live VPS before/after recorded on the PR.

* fix(sse): cline forceStream must stream upstream only, keep client JSON

The #2081 wiring fed providerRequiresStreaming into resolveStreamFlag,
forcing the client-facing stream flag to true for forceStream providers.
That skips the if(!stream) branch that drains a forced upstream SSE and
converts it back to JSON, so a stream:false caller (model-test button,
plain JSON API) got STREAM_EARLY_EOF instead of a JSON body.

Keep providerRequiresStreaming only on upstreamStream (force upstream to
stream); leave the client-facing stream as the client sent it, so
readNonStreamingResponseBody accumulates the SSE into JSON. The promised
handleForcedSSEToJson (#2081 comment) was never implemented — this uses
the existing non-streaming SSE-buffering path (same as isClaudeCodeCompatible).

Live-verified on VPS: cline stream:true worked, stream:false failed.
2026-07-05 02:29:28 -03:00
Diego Rodrigues de Sa e Souza
8a7b62ec85 fix(dashboard): remove the always-on Auto-Routing (combo) banner from the home page (#6164)
The blue "Auto-Routing Active — OmniRoute is automatically routing requests
using combo-based strategies" banner was rendered unconditionally on the home
page (`/home`, the default dashboard landing) — it did NOT reflect whether
auto-routing was actually active, and reappeared on every fresh browser / private
window / cleared localStorage (dismissal is stored per-browser). It added noise
to the landing page without conveying live state.

Remove it: drop the <AutoRoutingBanner /> usage + import from home/page.tsx and
delete the now-unused component and its test.
2026-07-05 02:29:24 -03:00
Diego Rodrigues de Sa e Souza
e44f125992 fix(dashboard): stop model-test error freezing the page (React #31 object toast) (#6161)
Clicking 'test' on a provider model (e.g. a ClinePass flash model) could freeze
the entire dashboard. Root cause: POST /api/models/test returned an OBJECT in
`error` on the Zod-validation and invalid-JSON paths (`validation.error.format()`
/ a details object). The client does `notify.error(data.error)`, and
NotificationToast renders the message directly as a React child — an object throws
React #31 ('Objects are not valid as a React child'), crashing the tree = frozen
page instead of a toast.

Fixed in three layers (defense in depth):
1. Server (root cause): /api/models/test now returns a STRING `error` on every
   path — flattens Zod issues to text, returns 'Invalid JSON body' for bad JSON.
2. Client: onTestModel funnels the response through extractApiErrorMessage() so any
   object-shaped error is coerced to a string before notify.error.
3. Toast: NotificationToast coerces title/message via toToastText() — a resilient
   catch-all so no future caller can freeze the page with a non-string.

Tests (Rule #18, both node:test / blocking suite):
- tests/unit/models-test-error-shape.test.ts — asserts STRING error on Zod-fail,
  missing-field, and invalid-JSON (fails on the pre-fix route: 3/3 red -> green).
- tests/unit/notification-toast-coercion.test.ts — toToastText coercion matrix.
2026-07-05 02:29:20 -03:00
Diego Rodrigues de Sa e Souza
cf6c2798b4 fix(oauth): extract keychain-import-only guard to restore file-size freeze (base-red) (#6158)
`src/app/api/oauth/[provider]/[action]/route.ts` grew to 959 lines, past its
frozen cap of 924 (`check:file-size` → Fast Quality Gates red on release/v3.8.44).
The growth came from #6054 (graceful 400 for keychain-import-only providers / zed):
a doc block, two Sets (KEYCHAIN_IMPORT_ONLY_PROVIDERS, OAUTH_FLOW_ACTIONS) and a
keychainImportOnlyResponse() helper, plus two duplicated guard blocks in GET/POST.

That is a cohesive, self-contained leaf, so extract it to a new
`keychainImportOnly.ts` exposing `keychainImportOnlyGuard(provider, action)`
(returns the 400 NextResponse or null). The two route callsites collapse to a
2-line guard each. route.ts: 959 -> 918 (< 924, freeze restored). No behavior
change.

Tests (Rule #8/#18):
- Existing tests/unit/oauth-keychain-import-only-6041.test.ts (route-level GET/POST
  zed 400) still pass unchanged — behavior preserved.
- New tests/unit/oauth-keychain-import-only-guard.test.ts pins the extracted guard
  in isolation (zed+flow -> 400, normal provider -> null, zed+non-flow -> null).
2026-07-05 02:29:16 -03:00
Diego Rodrigues de Sa e Souza
1473261c4c fix(backend): distinct max_input_tokens for GPT-family models (#6191) (#6230) 2026-07-04 23:02:55 -03:00
Diego Rodrigues de Sa e Souza
adde9e4bae fix(providers): refresh stale NVIDIA NIM model registry (#6108) (#6223) 2026-07-04 23:01:28 -03:00
Diego Rodrigues de Sa e Souza
cbc16af286 fix(backend): record reasoning source for zero-metered reasoning models (#6187) (#6229) 2026-07-04 22:59:08 -03:00
Diego Rodrigues de Sa e Souza
670d502bd9 fix(auth): clear error for stale-key decryption failures (#6148) (#6226) 2026-07-04 22:58:31 -03:00
Diego Rodrigues de Sa e Souza
201908df5e fix(backend): system-first memory injection for strict providers (#6135) (#6225) 2026-07-04 22:57:56 -03:00
Diego Rodrigues de Sa e Souza
8cb7f00821 fix(services): 9Router embed route + pre-spawn port probe (#6205) (#6227) 2026-07-04 22:56:22 -03:00
Diego Rodrigues de Sa e Souza
c04ce386f1 fix(mcp): forward extra context through static tool loops (#6178) (#6228) 2026-07-04 22:54:32 -03:00
Diego Rodrigues de Sa e Souza
2d5bd41261 fix(api): stabilize relay SSRF-guard binding for minified builds (#6149) (#6224) 2026-07-04 22:24:47 -03:00
Diego Rodrigues de Sa e Souza
1e59d14143 docs(changelog): v3.8.45 bullets for the tests+quality+CI pipeline overhaul (#6214, #6215, #6218)
i18n CHANGELOG mirrors intentionally left to the release reconciliation
(release:sync-changelog-i18n), per cycle practice.
2026-07-04 21:26:19 -03:00
Diego Rodrigues de Sa e Souza
059dbe9f13 feat(quality): no-new-warnings por PR — ESLint bulk suppressions + lint-guard fork-condicional (Pacote 4) (#6218)
* feat(quality): no-new-warnings per PR via native ESLint bulk suppressions

Pacote 4 do plano mestre testes+CI (aprovado 2026-07-04). O ratchet de
eslintWarnings so rodava no CI pesado (release-PR) -> o drift acumulava invisivel
e explodia na release (+41/+37/+88 por ciclo, rebaselinado as cegas — historico
no proprio quality-baseline.json). Modelo novo (SonarSource Clean-as-You-Code +
ESLint bulk suppressions nativo >=9.24):

- config/quality/eslint-suppressions.json congela a divida existente por
  arquivo+regra: 476 arquivos / 4.273 violacoes.
- npm run lint + lint-staged (pre-commit) + novo job lint-guard no quality.yml
  rodam suppressions-aware: violacao NOVA fica vermelha NO PR que a introduz
  (bulk suppressions ainda eleva estouros de baseline por arquivo a error).
- 3 regras warn promovidas a error em src/** (react-hooks/exhaustive-deps,
  @next/next/no-img-element, import/no-anonymous-default-export) — divida
  existente congelada, ocorrencia nova = erro imediato.
- collect-metrics mede sob o baseline congelado -> a metrica eslintWarnings
  vira 'divida liquida nova' (~0 em regime); baseline apertado 4279->0 no mesmo
  PR (exigencia do require-tighten). Aperto do ESTOQUE congelado: npx eslint .
  --prune-suppressions na reconciliacao da release.
- Principio Zero: lint-guard usa continue-on-error para PR de FORK (report-only;
  a campanha /green-prs aplica o fix via co-autoria) — bloqueante so para
  branches internas, a origem real do drift.

Validacao: negativo (any novo em tests/) exit 1; negativo (img em src/, regra
promovida) exit 1; positivo escopado exit 0; baseline gerado por --suppress-all
no tip (tree inteiro passa por construcao); YAML js-yaml ok.

* fix(quality): clear the 6 residual warnings so lint-guard runs clean at --max-warnings 0

The committed baseline still let 6 warnings through the lint-guard gate:
5 now-unused inline eslint-disable directives (the file-level suppressions
made them redundant — removed via eslint --fix, suppressions regenerated to
absorb the re-exposed occurrences) and 1 anonymous default export in
tests/load/k6-soak.js (outside the src/** severity-override scope — named
the k6 scenario function instead).

Verified on the clean tree: lint-guard exit=0; any-canary (new 'const x: any'
in open-sse) exit=1 — the gate bites on NEW violations while the 4,273
frozen ones stay suppressed (476 files).

* fix(ci): lint-guard continue-on-error must be boolean on non-PR events

github.event.pull_request is undefined on workflow_dispatch — the bare property
expression made the job fail at plan time (run 28722888456: 4 jobs green, run red,
lint-guard never materialized). Guard with event_name check so the expression is
always boolean: PR de fork = report-only (Principio Zero), resto = blocking.
2026-07-04 21:25:17 -03:00
Diego Rodrigues de Sa e Souza
5a4bde1879 ci: dedup heavy pipeline — compat to nightly, coverage folded into unit shards, i18n single job, draft-skip (#6215)
Pacote 2+3-ci do plano mestre testes+CI (aprovado 2026-07-04). O CI pesado rodava a
suite unit 4x por sync da release-PR (95 jobs, 208 min-maquina) e o ciclo v3.8.44
disparou 123 desses runs (88 cancelados, 0 uteis) porque a release-PR viva fica
aberta o ciclo inteiro.

- D2: matrizes Node 24/26 (build + 8 jobs de teste, ~28% do custo por run) saem do
  per-sync e viram .github/workflows/nightly-compat.yml (diario, fail-fast off,
  resolve a release ativa como o nightly-release-green, abre issue de tracking em
  falha). ci.yml/ci-summary limpos das referencias.
- D3: a matrix Coverage Shard x8 (~18% do custo) e eliminada — o job test-unit roda
  os MESMOS shards sob c8/NODE_V8_COVERAGE e sobe os artifacts coverage-shard-N; o
  job de merge (test-coverage) so repontou needs (padrao do CI do nodejs/node).
  timeout test-unit 15->25min pelo overhead de instrumentacao.
- D4: a matrix i18n de ~40 jobs de <1min (saturava sozinha os 20 slots de
  concorrencia da conta Free) vira 1 job que itera os idiomas com ::group:: por
  idioma e artifact unico com resultados nomeados por idioma (antes 40 result.txt
  colidiam no merge-multiple do ci-summary).
- P3: jobs pesados pulam pull_requests DRAFT (predicado em 10 jobs-raiz; o resto
  pula pela cadeia de needs; ci-summary segue rodando como sinal unico) — a skill
  /generate-release ja abre a release-PR viva como draft e flipa ready no 0a.0a
  (commit eb04fc5 no repo .agents/skills).
- C5 (CodeQL schedule) NAO incluido: bloqueado na acao do dono Settings -> CodeQL
  Default->Advanced (documentado no proprio codeql.yml).

Validacao: js-yaml parse ok; check:workflows zizmor 156 < baseline 159 (ratchet
verde); validacao de execucao = workflow_dispatch deste ci.yml neste branch ate
package-artifact + electron-package-smoke verdes (registrada no PR).
2026-07-04 21:24:10 -03:00
Diego Rodrigues de Sa e Souza
0757503eae perf(test): tsx/esm loader + tsx 4.23 + órfãos recuperados + CI via npm scripts (plano testes+CI, Pacote 1) (#6214)
* perf(test): tsx/esm loader, tsx 4.23 bump, orphan tests recovered, CI runs npm scripts

Pacote 1 (quick wins) do plano mestre testes+CI:

- Swap --import tsx -> --import tsx/esm on the 19 test scripts: the repo is pure
  ESM and the CJS hook costs ~1.3s PER test process (2,462 processes/run).
  Measured: bootstrap 2.3s -> 1.1s; real-suite A/B (tests/unit/db, 12 files)
  22.2s -> 14.1s wall (-36%), 82/82 pass. Non-test scripts keep the full hook.
- Bump tsx ^4.22.3 -> ^4.23.0 (fix for privatenumber/tsx#809 startup regression;
  helps module resolution on big graphs — hook cost unchanged, honest note).
- Recover 22 ORPHAN test files (tests/unit/feature-triage/*.test.mjs, 53 cases,
  53/53 pass) that matched no glob and ran in NO CI job; drop the dead
  'executors' dir from the braces glob.
- Single source of truth for the unit-suite invocation: new test:unit:ci:shard
  (shard via TEST_SHARD env) called by ci.yml test-unit/node24/node26/coverage
  and quality.yml fast-unit — closing two silent drifts: CI was NOT importing
  setupPolyfill.ts, and the fast path glob OMITTED tests/unit/memory + usage.
  quality.yml TIA step + ci.yml test-integration get the tsx/esm swap only.

Validation: full unit suite 21,153 tests / 21,135 pass / 13 skip (5 fails =
known load-flake family, 10/10 green rerun isolated); vitest 237/237; smoke
db+feature-triage 135/135. Record run 2 = ci.yml workflow_dispatch on the
stacked pacote-2 branch (clean runners).

* fix(test): dashboard UI tests keep full tsx hook; recover 15 more .mjs orphans; extend discovery gate to .mjs

Follow-up do dispatch de validacao (run 28720431562), que pegou 2 problemas reais:

1. tests/unit/dashboard/** (11 arquivos, 102 casos) importam componentes React cujo
   grafo puxa @lobehub/icons — o build es/ dele faz require() interno de arquivos com
   sintaxe ESM, que so funciona com o patch CJS do tsx (sem ele: SyntaxError
   'Unexpected token export' no CI; local vira crawl de ~60s/arquivo). Esses 11
   arquivos rodam agora numa 2a invocacao com --import tsx COMPLETO (mesmo shard),
   e o resto da suite mantem tsx/esm (-50% bootstrap). Validado: 102/102.
2. check:test-discovery falhou porque ancorava textualmente os globs nos workflows —
   COLLECTORS atualizado p/ o modelo fonte-unica (ancora = nome do script
   test:unit:ci:shard nos workflows) + varredura ESTENDIDA a .test.mjs, que era o
   ponto cego que deixou os orfaos apodrecerem. A extensao revelou +15 orfaos .mjs
   (top-level + db/) alem dos 22 de feature-triage — TODOS religados via glob
   tests/unit/**/*.test.mjs (171/171 pass). Um deles (encryption-error-handling)
   codificava o contrato PRE-hardening (decrypt falho retornava ciphertext cru —
   vazamento); alinhado ao contrato shipped (null + log) com comentario.

Gate: [test-discovery] OK — 2892 arquivos, 22 collectors, 60 orfaos congelados
(divida rastreada, shrink-only).
2026-07-04 21:21:23 -03:00
Diego Rodrigues de Sa e Souza
bb62c2a618 chore(release): parallel-cycle flow — sync-next-cycle script + Hard Rule #21 semantics (#6203)
Integrated into release/v3.8.45
2026-07-04 15:17:08 -03:00
Diego Rodrigues de Sa e Souza
7a098a0d85 chore(release): open v3.8.45 development cycle 2026-07-04 14:31:08 -03:00
1351 changed files with 75374 additions and 9217 deletions

View File

@@ -84,10 +84,14 @@ PORT=20128
# API_HOST=0.0.0.0
# DASHBOARD_PORT=20128
# Connection backpressure: cap concurrent in-flight chat connections (503 + Retry-After when full).
# Used by: src/sse/utils/backpressure.ts — disabled when unset/0.
# OMNI_MAX_CONCURRENT_CONNECTIONS=0
# Port for the real-time WebSocket live monitoring server.
# Used by: src/server/ws/liveServer.ts, src/app/api/v1/ws/route.ts
# Default: 20129
# LIVE_WS_PORT=20129
# Default: 20132
# LIVE_WS_PORT=20132
# Bind address for the live WebSocket server.
# Default: 127.0.0.1 (loopback only). Set to 0.0.0.0 to expose on LAN —
@@ -112,16 +116,14 @@ PORT=20128
# Public URL for the live dashboard WebSocket (client-side, browser only).
# Set this when fronting the WS server with a reverse proxy or Cloudflare Tunnel.
# The browser will connect to this URL instead of ws://hostname:20129.
# The /live-ws path is already proxied from the main app (port 20128) to the
# live WS server (port 20129) by scripts/dev/standalone-server-ws.mjs.
# Used by: src/hooks/useLiveDashboard.ts
# The browser will connect to this URL instead of ws://hostname:20132.
# The path portion of this URL (e.g. ws://localhost:20132/live-ws -> /live-ws) is also used by the dev proxy
# (scripts/dev/standalone-server-ws.mjs) and the handshake response to route
# WebSocket upgrades. Default path: /live-ws.
# Used by: src/hooks/useLiveDashboard.ts, src/app/api/v1/ws/route.ts,
# scripts/dev/standalone-server-ws.mjs, and scripts/start-ws-server.mjs.
# Example: NEXT_PUBLIC_LIVE_WS_PUBLIC_URL=wss://ws.my-ai.com/live-ws
# NEXT_PUBLIC_LIVE_WS_PUBLIC_URL=
# Disable the standalone live WebSocket helper used by scripts/start-ws-server.mjs.
# Used by: scripts/start-ws-server.mjs (CI/embedded harness toggle).
# OMNIROUTE_DISABLE_LIVE_WS=0
# NEXT_PUBLIC_LIVE_WS_PUBLIC_URL=ws://localhost:20132/live-ws
# Enable the real-time dashboard WebSocket server.
# Used by: src/server/ws/liveServer.ts, scripts/start-ws-server.mjs
@@ -174,6 +176,12 @@ OMNIROUTE_USE_TURBOPACK=1
# hints in production logs.
# OMNIROUTE_PROXY_FETCH_DEBUG=true
# Set to any non-empty value to emit `[omniroute completion]` diagnostics from
# the CLI shell-completion cache paths (read/refresh/write) in
# bin/cli/commands/completion.mjs. Off by default — these caches fail silently
# so a missing/corrupt cache never breaks tab-completion.
# OMNIROUTE_DEBUG_COMPLETION=1
# Docker production port mappings (docker-compose.prod.yml only).
# These set the HOST-side published ports. Container ports use PORT/API_PORT.
# PROD_DASHBOARD_PORT=20130
@@ -191,9 +199,9 @@ OMNIROUTE_USE_TURBOPACK=1
# the machine name by bash/zsh. The .env loader cannot override it (first-wins
# semantics). Use OMNIROUTE_SERVER_HOST instead for `omniroute serve`.
# See: https://github.com/diegosouzapw/OmniRoute/issues/6194
#HOST=0.0.0.0
#HOSTNAME=127.0.0.1
#OMNIROUTE_SERVER_HOST=0.0.0.0
# HOST=0.0.0.0
# HOSTNAME=127.0.0.1
# OMNIROUTE_SERVER_HOST=0.0.0.0
# Environment mode — affects Next.js behavior, logging verbosity, and caching.
# Values: production | development | Default: production
@@ -205,6 +213,17 @@ NODE_ENV=production
# gives the correct fix instructions (podman unshare chown vs sudo chown).
CONTAINER_HOST=docker
# Container runtime override for skill sandboxing.
# Used by: src/lib/skills/sandbox.ts + src/lib/skills/containerProvider.ts
# Values: auto | docker | apple | wsl | orbstack | podman
# - auto: OS-aware auto-detect (apple/orbstack on macOS, wsl on Windows, podman on Linux)
# - apple: Apple Container (native OCI on macOS 26+)
# - wsl: WSL Container CLI (wslc.exe on Windows)
# - orbstack: OrbStack (high-perf Linux VM + docker shim on macOS)
# - podman: Podman (rootless, daemonless)
# - docker: Docker (default fallback)
SKILLS_SANDBOX_RUNTIME=auto
# ═══════════════════════════════════════════════════════════════════════════════
# 4. SECURITY & AUTHENTICATION
# ═══════════════════════════════════════════════════════════════════════════════
@@ -966,7 +985,7 @@ GITHUB_OAUTH_CLIENT_ID=Iv1.b507a08c87ecfe98
# Used by: open-sse/executors/base.ts — buildHeaders() dynamic lookup.
# Update these when providers release new CLI versions to avoid blocks.
CLAUDE_USER_AGENT="claude-cli/2.1.195 (external, cli)"
CLAUDE_USER_AGENT="claude-cli/2.1.207 (external, cli)"
# Disable the deterministic tool-name cloak applied on both Anthropic-bound paths
# (executors/base.ts native OAuth + executors/cliproxyapi.ts CLIProxyAPI) —
@@ -975,7 +994,7 @@ CLAUDE_USER_AGENT="claude-cli/2.1.195 (external, cli)"
# stream with a misleading 400 out-of-extra-usage placeholder. Set to true to
# forward the original names verbatim (debugging only).
# CLAUDE_DISABLE_TOOL_NAME_CLOAK=false
CODEX_USER_AGENT="codex-cli/0.142.0 (Windows 10.0.26200; x64)"
CODEX_USER_AGENT="codex-cli/0.144.1 (Windows 10.0.26200; x64)"
GITHUB_USER_AGENT="GitHubCopilotChat/0.54.0"
ANTIGRAVITY_USER_AGENT="antigravity/2.0.1 linux/arm64 google-api-nodejs-client/10.3.0"
KIRO_USER_AGENT="AWS-SDK-JS/3.0.0 kiro-ide/1.0.0"
@@ -996,7 +1015,7 @@ CURSOR_USER_AGENT="Cursor/3.4"
# Override Codex client version sent in headers independently of the
# CODEX_USER_AGENT string. Used by: open-sse/config/codexClient.ts.
# CODEX_CLIENT_VERSION=0.142.0
# CODEX_CLIENT_VERSION=0.144.1
# Kill-switch to strip non-standard `codex.*` SSE events (e.g. codex.rate_limits)
# from the Codex Responses stream. These frames break the OpenAI SDK's
@@ -1512,6 +1531,11 @@ APP_LOG_TO_FILE=true
# Timeout for fast-fail health checks (ms). Default: 2000
# PROXY_FAST_FAIL_TIMEOUT_MS=2000
# Time window (hours) for calculating the average latency of candidate proxies
# in the latency-optimized pool strategy. Default: 3
# Used by: src/lib/db/proxies.ts
# PROXY_LATENCY_WINDOW_HOURS=3
# Health check result cache TTL (ms). Default: 30000 (30s)
# PROXY_HEALTH_CACHE_TTL_MS=30000
@@ -1630,10 +1654,19 @@ APP_LOG_TO_FILE=true
# Used by: open-sse/utils/cursorImages.ts.
# CURSOR_IMAGE_FETCH_TIMEOUT_MS=15000
# Cursor state DB path override (for cursor version detection).
# Cursor state DB path override (for IDE cursor version detection).
# Used by: open-sse/utils/cursorVersionDetector.ts. Default: probed automatically.
# CURSOR_STATE_DB_PATH=
# Cursor Agent CLI build id for AgentService/Run impersonation (YYYY.MM.DD-<hash>).
# Used by: open-sse/utils/cursorAgentCliVersion.ts. Default: detect local install, else pin.
# CURSOR_AGENT_CLI_VERSION=2026.07.08-0c04a8a
# Cursor Agent CLI data directory override (versions live under <dir>/versions/).
# Used by: open-sse/utils/cursorAgentCliVersion.ts. Default: ~/.local/share/cursor-agent (unix)
# or %LOCALAPPDATA%\cursor-agent (win32). Official agent CLI also honors this var.
# CURSOR_DATA_DIR=
# Direct Cursor bearer token used by scripts/ad-hoc/cursor-tap.cjs (developer tooling).
# CURSOR_TOKEN=
@@ -1815,6 +1848,15 @@ APP_LOG_TO_FILE=true
# SKILLS_SANDBOX_NETWORK_ENABLED=0
# SKILLS_ALLOWED_SANDBOX_IMAGES=
# Container runtime used by the skill sandbox. Accepted values:
# auto — pick the best installed runtime per host OS (default)
# docker — Docker Engine / Docker Desktop
# apple — Apple Container (macOS native, micro-VM)
# wsl — WSL Container (Windows native via wslc.exe)
# orbstack — OrbStack (high-perf Linux VM + docker shim on macOS)
# podman — Podman (rootless, daemonless)
# SKILLS_SANDBOX_RUNTIME=auto
# ═══════════════════════════════════════════════════════════════════════════════
# 25. TEST & E2E
# ═══════════════════════════════════════════════════════════════════════════════
@@ -1860,7 +1902,7 @@ APP_LOG_TO_FILE=true
# OMNIROUTE_TRANSLATION_API_URL=
# Bearer token for the translation backend (NEVER commit a real key here).
# OMNIROUTE_TRANSLATION_API_KEY=
# Model id, e.g. gpt-4o-mini or cx/gpt-5.4-mini.
# Model id, e.g. gpt-4o-mini or cx/gpt-5.6-sol.
# OMNIROUTE_TRANSLATION_MODEL=gpt-4o-mini
# Per-request timeout in milliseconds (default 60000).
# OMNIROUTE_TRANSLATION_TIMEOUT_MS=60000
@@ -2068,3 +2110,24 @@ QUOTA_STORE_DRIVER=sqlite # sqlite | redis
# BIFROST_API_KEY=
# BIFROST_STREAMING_ENABLED=true
# BIFROST_TIMEOUT_MS=30000
# ─────────────────────────────────────────────────────────────────────────────
# Account rotation config (operator-managed; consumed by open-sse/services/rotationConfig.ts)
# Lets a supervising front-end mirror its rotation rules onto the backend's account-fallback
# engine. All optional; defaults preserve the historical behavior.
# ─────────────────────────────────────────────────────────────────────────────
# OMNIROUTE_ROTATION_ENABLED=true
# OMNIROUTE_ROTATION_RATE_LIMIT_RESET_SECONDS=0
# OMNIROUTE_ROTATION_DISABLE_TAG_WITHOUT_RESET=true
# OMNIROUTE_ROTATE_ON_429=true
# OMNIROUTE_ROTATE_429_THRESHOLD=1
# OMNIROUTE_ROTATE_429_WINDOW_SECONDS=120
# OMNIROUTE_ROTATE_ON_500=true
# OMNIROUTE_ROTATE_500_THRESHOLD=1
# OMNIROUTE_ROTATE_500_WINDOW_SECONDS=120
# OMNIROUTE_ROTATE_ON_502=true
# OMNIROUTE_ROTATE_502_THRESHOLD=1
# OMNIROUTE_ROTATE_502_WINDOW_SECONDS=120
# OMNIROUTE_ROTATE_ON_400=false
# OMNIROUTE_ROTATE_400_THRESHOLD=1
# OMNIROUTE_ROTATE_400_WINDOW_SECONDS=120

View File

@@ -38,12 +38,17 @@ jobs:
with:
persist-credentials: false
fetch-depth: 0
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
- id: classify
env:
EVENT_NAME: ${{ github.event_name }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
# Single source of truth: scripts/quality/classify-pr-changes.mjs
# (unit-tested). Push/dispatch always enable every lane.
if [ "$EVENT_NAME" != "pull_request" ]; then
{
echo "code=true"
@@ -54,51 +59,18 @@ jobs:
exit 0
fi
code=false
docs=false
i18n=false
workflow=false
git diff --name-only "$BASE_SHA" "$HEAD_SHA" > changed-files.txt
while IFS= read -r file; do
case "$file" in
.github/workflows/*|.zizmor.yml)
workflow=true
code=true
;;
docs/*|*.md)
docs=true
;;
src/i18n/*|src/i18n/messages/*|scripts/i18n/*|config/i18n.json)
i18n=true
code=true
;;
src/*|open-sse/*|bin/*|electron/*|tests/*|scripts/*|package.json|package-lock.json|tsconfig*.json|next.config.*|vitest*.config.*|playwright.config.*)
code=true
;;
db/*|config/*)
code=true
;;
*)
code=true
;;
esac
done < changed-files.txt
{
echo "code=$code"
echo "docs=$docs"
echo "i18n=$i18n"
echo "workflow=$workflow"
} >> "$GITHUB_OUTPUT"
node scripts/quality/classify-pr-changes.mjs changed-files.txt >> "$GITHUB_OUTPUT"
lint:
name: Lint
runs-on: ubuntu-latest
needs: changes
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
# Path filter: pure docs / pure message-catalog PRs skip the code lint bag + typecheck.
# Existence reason of this job is code regression; docs/i18n have dedicated jobs.
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && needs.changes.outputs.code == 'true') }}
env:
# tsx gates below (known-symbols, route-guard-membership) import modules that
# open SQLite on load; provide DB env so a fresh CI DB initializes cleanly.
@@ -116,7 +88,27 @@ jobs:
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- run: npm run audit:deps
- run: npm run lint
- name: Restore ESLint file cache
uses: actions/cache@v6
with:
path: |
.eslintcache
.eslintcache-complexity
key: eslint-${{ runner.os }}-${{ hashFiles('eslint.config.mjs', 'eslint.complexity-ratchets.config.mjs', 'config/quality/eslint-suppressions.json', 'package-lock.json') }}
restore-keys: |
eslint-${{ runner.os }}-
# Single ESLint inventory (JSON) — quality-gate reuses the artifact instead of
# a second cold full-tree pass for eslintWarnings ratchet counts.
- name: ESLint (JSON report)
run: npm run lint:json
- name: Upload ESLint results
if: always()
uses: actions/upload-artifact@v7
with:
name: eslint-results
path: .artifacts/eslint-results.json
if-no-files-found: warn
retention-days: 7
- run: npm run check:cycles
- run: npm run check:route-validation:t06
- run: npm run check:any-budget:t11
@@ -137,16 +129,16 @@ jobs:
# check:docs-sync is run by the docs-sync-strict job (via check:docs-all) and the
# husky pre-commit hook; the standalone copy here was redundant (ROI dedup).
- run: npm run typecheck:core
# typecheck:noimplicit:core is a forward-looking gate (noImplicitAny).
# Run informationally for now — many pre-existing call sites still need
# explicit annotations; track in a dedicated follow-up.
- run: npm run typecheck:noimplicit:core
continue-on-error: true
# typecheck:noimplicit:core dropped from this job (2026-07 optimize):
# it was advisory (continue-on-error) and largely subsumed by the blocking
# check:type-coverage ratchet in quality-gate. Local: npm run typecheck:noimplicit:core.
quality-gate:
name: Quality Ratchet
runs-on: ubuntu-latest
needs: test-coverage
# needs lint so eslint-results artifact is available (same inventory as the
# blocking lint step). Allow lint failure so other ratchets still run.
needs: [changes, test-coverage, lint]
# Run even when test-coverage was SKIPPED/FAILED (e.g. a single flaky Coverage
# Shard breaks the shard→coverage→ratchet chain). The DETERMINISTIC ratchets
# (eslint / complexity / cognitive-complexity / duplication / codeql) do NOT need
@@ -155,7 +147,8 @@ jobs:
# release PR #4854, where the drift cascade only surfaced post-merge in #5029).
# The coverage.* metrics degrade gracefully: the download is continue-on-error and
# the ratchet runs with --allow-missing, so absent coverage is skipped, not failed.
if: ${{ !cancelled() && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) }}
# Path filter: code-only — pure docs/i18n PRs have nothing for these ratchets to guard.
if: ${{ !cancelled() && (github.event_name != 'pull_request' || (github.event.pull_request.draft == false && needs.changes.outputs.code == 'true' && (needs.lint.result == 'success' || needs.lint.result == 'failure'))) }}
# security-events: read lets the CodeQL ratchet read open code-scanning alerts
# via `gh api .../code-scanning/alerts`. contents: read keeps checkout working.
permissions:
@@ -170,6 +163,15 @@ jobs:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- name: Restore ESLint file cache
uses: actions/cache@v6
with:
path: |
.eslintcache
.eslintcache-complexity
key: eslint-${{ runner.os }}-${{ hashFiles('eslint.config.mjs', 'eslint.complexity-ratchets.config.mjs', 'config/quality/eslint-suppressions.json', 'package-lock.json') }}
restore-keys: |
eslint-${{ runner.os }}-
# Coverage mergeada (coverage-summary.json) p/ o ratchet de cobertura.
# continue-on-error: o artifact pode não existir se a job test-coverage foi
# SKIPPED (shard flaky). Nesse caso collect-metrics pula coverage.* (ausente sem
@@ -180,6 +182,13 @@ jobs:
with:
name: coverage-report
path: coverage/
# Prefer lint job's ESLint JSON (one inventory, two consumers).
- name: Download ESLint results
continue-on-error: true
uses: actions/download-artifact@v8
with:
name: eslint-results
path: .artifacts/
- run: npm run quality:collect
# Catraca: falha se qualquer métrica regredir vs quality-baseline.json (commitado).
# Hoje: contagem de warnings do ESLint. Fase 4 estende com cobertura (lida do
@@ -201,15 +210,13 @@ jobs:
# para não pesar no caminho crítico do lint.
- name: Duplication ratchet
run: npm run check:duplication
- name: Complexity ratchet
run: npm run check:complexity
# Fase 7 INT: dead-code, cognitive-complexity, type-coverage promovidos de
# advisory (quality-extended) para BLOQUEANTES aqui. Os 3 leem seus baseline
# de quality-baseline.json e saem 1 em regressão.
# Complexity + cognitive: one ESLint walk, two independent baselines (by ruleId).
- name: Complexity + cognitive ratchets
run: npm run check:complexity-ratchets
# Fase 7 INT: dead-code, type-coverage promovidos de advisory (quality-extended)
# para BLOQUEANTES aqui. cognitive-complexity is folded into the step above.
- name: Dead-code ratchet (knip)
run: npm run check:dead-code
- name: Cognitive complexity ratchet (sonarjs)
run: npm run check:cognitive-complexity
- name: Type coverage ratchet
run: npm run check:type-coverage
- name: Compression budget ratchet (F2.4 / N4)
@@ -247,9 +254,11 @@ jobs:
quality-extended:
name: Quality Gates (Extended)
runs-on: ubuntu-latest
needs: changes
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
# Path filter: code-only (scanners/ratchets target production surface).
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && needs.changes.outputs.code == 'true') }}
steps:
# fetch-depth: 0 — the OpenAPI breaking-change gate (oasdiff) reads the base
# spec via `git show <base_ref>:docs/openapi.yaml`; a shallow clone
@@ -357,9 +366,11 @@ jobs:
docs-sync-strict:
name: Docs Sync (Strict)
runs-on: ubuntu-latest
needs: changes
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
# Run when docs OR code change: API/route code can break doc/OpenAPI contract gates.
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && (needs.changes.outputs.docs == 'true' || needs.changes.outputs.code == 'true')) }}
steps:
- uses: actions/checkout@v7
with:
@@ -379,19 +390,20 @@ jobs:
run: npm run check:openapi-coverage
- name: OpenAPI security-tier consistency (advisory)
run: npm run check:openapi-security-tiers
- name: OpenAPI spec paths resolve to real routes (anti-hallucination)
run: npm run check:openapi-routes
- name: Doc /api refs resolve to real routes (anti-hallucination)
run: npm run check:docs-symbols
# One FS inventory of src/app/api for both anti-hallucination directions.
- name: API docs refs (openapi + prose → routes)
run: npm run check:api-docs-refs
- name: i18n translation drift (warn)
run: node scripts/i18n/check-translation-drift.mjs --warn
docs-lint:
name: Docs Lint (prose — advisory)
runs-on: ubuntu-latest
needs: changes
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
# Prose/markdown only — skip when the PR has no doc surface.
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && needs.changes.outputs.docs == 'true') }}
# Advisory (warning-first): prose/markdown style must not block merges while the
# existing doc corpus is brought up to style. Promote to blocking once it converges.
continue-on-error: true
@@ -418,9 +430,11 @@ jobs:
i18n-ui-coverage:
name: i18n UI Coverage
runs-on: ubuntu-latest
needs: changes
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
# UI keys move with dashboard code OR message catalogs.
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && (needs.changes.outputs.i18n == 'true' || needs.changes.outputs.code == 'true')) }}
steps:
- uses: actions/checkout@v7
with:
@@ -440,9 +454,11 @@ jobs:
i18n:
name: i18n Validation (all languages)
runs-on: ubuntu-latest
needs: changes
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
# Message-catalog / i18n-tooling only — pure code without i18n surface skips this lane.
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && needs.changes.outputs.i18n == 'true') }}
continue-on-error: true
steps:
- uses: actions/checkout@v7
@@ -961,7 +977,7 @@ jobs:
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- name: Cache Playwright browsers
uses: actions/cache@v6.1.0
uses: actions/cache@v6
with:
path: ~/.cache/ms-playwright
key: playwright-chromium-${{ runner.os }}-${{ hashFiles('package-lock.json') }}

View File

@@ -22,10 +22,10 @@ jobs:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
- uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
with:
languages: javascript-typescript
queries: security-extended
- uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
- uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
with:
category: "/language:javascript-typescript"

View File

@@ -201,6 +201,12 @@ jobs:
[ -f "$file" ] && cp "$file" "../../release-assets/OmniRoute.exe" && break
done
fi
# electron-updater manifests (latest.yml / latest-mac.yml / latest-linux.yml)
# must be published alongside the installers, or autoUpdater fails with
# "Cannot find latest.yml in the latest release artifacts" (#6766).
for file in latest*.yml; do
[ -f "$file" ] && cp "$file" ../../release-assets/
done
- name: Upload artifacts
uses: actions/upload-artifact@v7
@@ -263,6 +269,7 @@ jobs:
release-assets/*.AppImage
release-assets/*.deb
release-assets/*.blockmap
release-assets/*.yml
release-assets/*.source.tar.gz
release-assets/*.source.zip
env:

View File

@@ -97,7 +97,12 @@ jobs:
set +e
# --hermetic: scrub live-test trigger vars (self-hosted runner may carry
# operator env; hosted ignores the unknown flag before #6300 lands).
node scripts/quality/validate-release-green.mjs --json --with-build --hermetic \
# --full-ci: ALSO run every static gate from ci.yml's gate jobs (lint,
# quality-gate, quality-extended, docs-sync-strict, pr-test-policy). PRs into
# release/** only get the fast-gates, so these accrue silently and explode in
# layers on the release PR (v3.8.46: 11 static base-reds leaked). Running them
# nightly opens the tracking issue the moment one lands, not at release time.
node scripts/quality/validate-release-green.mjs --json --with-build --hermetic --full-ci \
1> release-green.json 2> release-green.log
echo "exit=$?" >> "$GITHUB_OUTPUT"
echo "------- report -------"
@@ -137,7 +142,7 @@ jobs:
- name: Upload report artifact
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: release-green-report
path: |

View File

@@ -21,9 +21,73 @@ env:
CI_NODE_VERSION: "24"
jobs:
# Same classifier as ci.yml (scripts/quality/classify-pr-changes.mjs) so PR→release
# path filters share existence reasons: code / docs / i18n / workflow.
changes:
name: Change Classification
runs-on: ubuntu-latest
outputs:
code: ${{ steps.classify.outputs.code }}
docs: ${{ steps.classify.outputs.docs }}
i18n: ${{ steps.classify.outputs.i18n }}
workflow: ${{ steps.classify.outputs.workflow }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
fetch-depth: 0
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
- id: classify
env:
EVENT_NAME: ${{ github.event_name }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
if [ "$EVENT_NAME" != "pull_request" ]; then
{
echo "code=true"
echo "docs=true"
echo "i18n=true"
echo "workflow=true"
} >> "$GITHUB_OUTPUT"
exit 0
fi
git diff --name-only "$BASE_SHA" "$HEAD_SHA" > changed-files.txt
node scripts/quality/classify-pr-changes.mjs changed-files.txt >> "$GITHUB_OUTPUT"
# Docs/OpenAPI contract gates only — existence reason is doc accuracy + route refs.
# Split out of fast-gates so pure-docs PRs skip typecheck/unit while still validating docs.
docs-gates:
name: Docs Gates (fast-path)
needs: changes
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && (needs.changes.outputs.docs == 'true' || needs.changes.outputs.code == 'true')) }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
# One walk of src/app/api for openapi-routes + docs-symbols (both still fail independently).
- run: npm run check:api-docs-refs
- name: Docs accuracy (fabricated-docs + i18n mirrors, strict)
run: npm run check:docs-all
fast-gates:
name: Fast Quality Gates
runs-on: ubuntu-latest
needs: changes
# Code surface only — pure docs/i18n PRs skip this bag (docs-gates covers docs).
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && needs.changes.outputs.code == 'true') }}
# Dynamic runner (same rule as ci.yml): use the self-hosted VPS pool only when the
# release captain has USE_VPS_RUNNER=true AND this is not a fork PR (own-origin
# branches only — a fork PR must never execute on the LAN runner). Var unset/false
# or a fork PR falls back to ubuntu-latest, so this is inert until the flag flips.
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
# tsx gates (known-symbols, route-guard-membership) import modules that open
# SQLite on load; provide DB env so a fresh CI DB initializes cleanly.
env:
@@ -40,12 +104,18 @@ jobs:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
- name: Restore ESLint file cache
uses: actions/cache@v6
with:
path: |
.eslintcache
.eslintcache-complexity
key: eslint-${{ runner.os }}-${{ hashFiles('eslint.config.mjs', 'eslint.complexity-ratchets.config.mjs', 'config/quality/eslint-suppressions.json', 'package-lock.json') }}
restore-keys: |
eslint-${{ runner.os }}-
- run: npm run check:provider-consistency
- run: npm run check:fetch-targets
- run: npm run check:openapi-routes
- run: npm run check:docs-symbols
- name: Docs accuracy (fabricated-docs + i18n mirrors, strict)
run: npm run check:docs-all
# docs-all / openapi-routes / docs-symbols live in docs-gates (path-filtered).
- run: npm run check:deps
- run: npm run check:file-size
- run: npm run check:error-helper
@@ -68,25 +138,21 @@ jobs:
# leaking into the npm tarball (v3.8.36: 6 ops bin/*.sh) per-PR instead of only on
# the release PR's heavy Package Artifact job.
- run: npm run check:pack-policy
# Complexity + cognitive-complexity ratchets on the fast-path (PR→release) so
# cycle drift is rebaselined PER-PR instead of cascading onto the release PR's
# Quality Ratchet (v3.8.36: +30 complexity / +15 cognitive surfaced only post-merge).
- run: npm run check:complexity
- run: npm run check:cognitive-complexity
# Complexity + cognitive-complexity: ONE ESLint walk (both baselines still
# enforced separately by ruleId). Avoids two cold tree walks on fast-path.
- run: npm run check:complexity-ratchets
- name: Typecheck (core)
run: npm run typecheck:core
# TIA: build the impact map at runtime (gitignored, ~21MB) and run only the
# unit tests impacted by this PR's changed files. Fail-safe runs the FULL
# unit suite on hub/unmapped changes — TIA accelerates, never replaces, the net.
# unit tests impacted by this PR's changed files. On hub/unmapped changes the
# selector returns __RUN_ALL__ — full-suite authority is the parallel
# `fast-unit` 4-shard job (test:unit:ci:shard; was 2-shard, #6781), NOT an
# unsharded re-run here. Stacking unsharded test:unit:ci on top of fast-unit
# doubled wall time (~16 min extra on ubuntu-latest) without extra coverage.
#
# BLOCKING (flipped 2026-06-17). The pre-existing release unit test-debt that kept
# this advisory was cleared: #4030 (16 Zod/registry reds, lossless restore) and
# #4063 (the last red — the LiveWS boot test — root-caused as a real event-loop
# stall in the WS sidecar, fixed + relocated to the integration suite). A full
# ci.yml run on release/v3.8.28 then showed all 8 unit shards green, so PR->release
# now blocks on unit-test regressions in the impacted set (typecheck:core already
# blocked above). Fail-safe still runs the FULL unit suite on hub/unmapped changes.
- name: Impacted unit tests (TIA, fail-safe full; blocking)
# BLOCKING for the *impacted subset* (flipped 2026-06-17). Fail-safe full
# coverage remains required via `Unit Tests fast-path` (fast-unit).
- name: Impacted unit tests (TIA subset; blocking)
env:
GITHUB_BASE_REF: ${{ github.base_ref }}
run: |
@@ -100,15 +166,39 @@ jobs:
# which must not happen on a blocking gate. DATA_DIR isolation keeps the parallel
# run race-free regardless of concurrency.
if echo "$SEL" | grep -q "__RUN_ALL__"; then
echo "Fail-safe: running FULL unit suite (CI concurrency)"; npm run test:unit:ci; exit $?
echo "Fail-safe: __RUN_ALL__ — deferring FULL unit suite to fast-unit (4-shard)."
echo "Not re-running unsharded test:unit:ci here (duplicate of fast-unit coverage)."
exit 0
fi
echo "Running impacted tests:"; echo "$SEL"
mapfile -t FILES <<< "$SEL"
node --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 "${FILES[@]}"
# Loader parity with test:unit:ci:shard (#6787): tests/unit/dashboard/** runs
# under `--import tsx` (CJS transform — required for ESM-only deep imports like
# @lobehub/icons/es/* reached via lobeProviderIcons.ts); everything else under
# `--import tsx/esm`. A single tsx/esm invocation false-reds every dashboard
# module-shape test the impact map selects ("Unexpected token 'export'").
DASH=(); REST=()
for f in "${FILES[@]}"; do
case "$f" in
tests/unit/dashboard/*) DASH+=("$f") ;;
*) REST+=("$f") ;;
esac
done
RC=0
if [ ${#REST[@]} -gt 0 ]; then
node --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 "${REST[@]}" || RC=$?
fi
if [ ${#DASH[@]} -gt 0 ]; then
node --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 "${DASH[@]}" || RC=$?
fi
exit $RC
fast-vitest:
name: Vitest (fast-path)
runs-on: ubuntu-latest
needs: changes
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && needs.changes.outputs.code == 'true') }}
# Dynamic runner — see fast-gates (own-origin + flag; fork/unset → ubuntu-latest).
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
env:
JWT_SECRET: ci-lint-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-lint-api-key-secret-long
@@ -125,12 +215,19 @@ jobs:
- run: npm run test:vitest
fast-unit:
name: Unit Tests fast-path (${{ matrix.shard }}/2)
runs-on: ubuntu-latest
name: Unit Tests fast-path (${{ matrix.shard }}/4)
needs: changes
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && needs.changes.outputs.code == 'true') }}
# Dynamic runner — see fast-gates (own-origin + flag; fork/unset → ubuntu-latest).
# This is the heaviest fast-path job; 4-way sharding (was 2, #6781) halves the
# critical path again (~8.5min → ~4.5min on ubuntu-latest; ~2min on the 8-slot
# runner box). Node's native --test-shard=N/total takes any denominator — only
# this matrix and the TEST_SHARD env below encode the shard count.
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
strategy:
fail-fast: false
matrix:
shard: [1, 2]
shard: [1, 2, 3, 4]
env:
JWT_SECRET: ci-lint-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-lint-api-key-secret-long
@@ -149,7 +246,7 @@ jobs:
# silenciosamente não rodavam no fast path) e o setupPolyfill não era importado.
- run: npm run test:unit:ci:shard
env:
TEST_SHARD: ${{ matrix.shard }}/2
TEST_SHARD: ${{ matrix.shard }}/4
# ── Pacote 4 (plano mestre testes+CI, aprovado 2026-07-04) ─────────────────────────
# No-new-warnings por PR via ESLint bulk suppressions nativo (>=9.24). O baseline
@@ -165,6 +262,8 @@ jobs:
# contribuidor NUNCA é bloqueado nem cobrado).
lint-guard:
name: No new ESLint warnings
needs: changes
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && needs.changes.outputs.code == 'true') }}
runs-on: ubuntu-latest
continue-on-error: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
steps:
@@ -176,8 +275,18 @@ jobs:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
- name: Restore ESLint file cache
uses: actions/cache@v6
with:
path: |
.eslintcache
.eslintcache-complexity
key: eslint-${{ runner.os }}-${{ hashFiles('eslint.config.mjs', 'eslint.complexity-ratchets.config.mjs', 'config/quality/eslint-suppressions.json', 'package-lock.json') }}
restore-keys: |
eslint-${{ runner.os }}-
- name: ESLint (baseline congelado — warning novo = vermelho)
run: npx eslint . --suppressions-location config/quality/eslint-suppressions.json --max-warnings 0
# lint:json writes the report; --max-warnings 0 keeps no-new-warnings policy.
run: npm run lint:json -- --max-warnings 0
# Merge-integrity: pega no PR os dois vazamentos crônicos de merge que hoje só
# explodem na release-PR. (1) CHANGELOG-eat — o auto-resolve do merge come
@@ -192,6 +301,8 @@ jobs:
# nunca é bloqueado.
merge-integrity:
name: Merge integrity (changelog + generated skills)
# Always on non-draft PRs — CHANGELOG/skills can break on docs-only merges too.
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
runs-on: ubuntu-latest
continue-on-error: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
env:

9
.gitignore vendored
View File

@@ -233,3 +233,12 @@ omniroute.md
# mise configuration
mise.toml
_artifacts/
.claude-flow/
# ESLint file cache (npm run lint --cache / complexity ratchets)
.eslintcache
.eslintcache-complexity
# CI/local quality artifacts (eslint-results.json, etc.)
.artifacts/

View File

@@ -1,7 +1,9 @@
#!/usr/bin/env sh
# .husky/pre-push — fast deterministic gates (<10s total)
# Intentionally excludes test:unit (slow; covered by CI pre-push remote run).
# Activated: 2026-06-13 (6A.12 — replaced commented-out test:unit stub)
# .husky/pre-push — intentionally light.
# any-budget + tracked-artifacts already run on pre-commit; re-running them on
# every push only doubles local wall time for the same existence reason (CI still
# enforces both). Keep this hook as a PATH/npm sanity check + reminder.
# Intentionally excludes test:unit / typecheck (slow; covered by CI).
if ! command -v npm >/dev/null 2>&1; then
echo "⚠️ npm not found in PATH — skipping pre-push hooks"
@@ -9,4 +11,5 @@ if ! command -v npm >/dev/null 2>&1; then
exit 0
fi
npm run check:any-budget:t11 && npm run check:tracked-artifacts
# No-op success: real local gates live in pre-commit; CI owns the rest.
exit 0

View File

@@ -1,2 +1,5 @@
# Long reference tables are manually aligned; formatting the whole file causes noisy diffs.
docs/reference/ENVIRONMENT.md
# Dense auto-generated free-tier budget rows (one object per line) — prettier multi-line expand blows past file-size cap 800.
open-sse/config/freeModelCatalog.data.ts

10
.vscode/settings.json vendored
View File

@@ -48,11 +48,19 @@
"**/.build",
"**/dist",
"**/coverage",
"**/.worktrees"
"**/.worktrees",
"**/.claude/worktrees",
"**/electron",
"**/_references",
"**/_mono_repo",
"**/_tasks"
]
},
// Para esconder os diretórios gerados da árvore do Explorer, descomente:
// (MANTIDO comentado — o dono precisa ver _references/_mono_repo/_tasks na árvore.
// A performance é resolvida por watcherExclude + search.exclude + tsserver, sem
// precisar escondê-los do Explorer.)
// "files.exclude": {
// "**/.worktrees": true,
// "**/coverage": true,

View File

@@ -23,7 +23,7 @@
"scripts": {
"build": "tsup",
"clean": "rm -rf dist",
"test": "node --import tsx/esm --test tests/scaffold.test.ts tests/auth.test.ts tests/options-schema.test.ts tests/multi-instance.test.ts tests/fetch-interceptor.test.ts tests/provider.test.ts tests/gemini-sanitize.test.ts tests/combos.test.ts tests/config-shim.test.ts tests/features.test.ts tests/usable-combo.test.ts tests/disk-snapshot-perms.test.ts tests/fork-features.test.ts tests/auto-combo-context.test.ts",
"test": "node --import tsx/esm --test tests/scaffold.test.ts tests/auth.test.ts tests/options-schema.test.ts tests/multi-instance.test.ts tests/fetch-interceptor.test.ts tests/provider.test.ts tests/gemini-sanitize.test.ts tests/combos.test.ts tests/config-shim.test.ts tests/features.test.ts tests/usable-combo.test.ts tests/disk-snapshot-perms.test.ts tests/fork-features.test.ts tests/auto-combo-context.test.ts tests/provider-id-routing.test.ts",
"prepublishOnly": "npm run clean && npm run build && npm test"
},
"keywords": [

View File

@@ -238,9 +238,26 @@ function trimLeadingDashes(value: string): string {
*/
export function resolveOmniRoutePluginOptions(
opts?: OmniRoutePluginOptions
): Required<Pick<OmniRoutePluginOptions, "providerId" | "displayName" | "modelCacheTtl">> &
Pick<OmniRoutePluginOptions, "baseURL" | "features"> {
): Required<
Pick<OmniRoutePluginOptions, "providerId" | "displayName" | "modelCacheTtl">
> & {
/**
* #6859: the UNPREFIXED provider id ("omniroute", "omniroute-preprod", …).
* `providerId` above is auto-prefixed with "opencode-" ONLY to satisfy OC
* 1.17.8+'s native-adapter gate ({openai, anthropic, opencode*}) — that
* prefixed value is OC-internal and must be used ONLY for AuthHook.provider
* and provider-registration keys (the OC config-hook top-level
* `provider.<id>` block). `omnirouteProviderId` MUST be used everywhere an
* identifier reaches or represents something OmniRoute's own server parses
* (model `id` prefix, `ModelV2.providerID`, combo catalog keys in the
* dynamic provider hook) — OmniRoute's `parseModel()` has no alias for
* "opencode-<x>", so a prefixed id there is unrecoverable and credential
* lookup fails with "No credentials for opencode-<x>".
*/
omnirouteProviderId: string;
} & Pick<OmniRoutePluginOptions, "baseURL" | "features"> {
const rawProviderId = opts?.providerId ?? OMNIROUTE_PROVIDER_KEY;
const omnirouteProviderId = trimLeadingOpencodePrefix(rawProviderId);
// OC 1.17.8+ native-adapter gate rejects providerID not in
// {openai, anthropic, opencode*}. Silently prefix so existing
// configs (providerId: "omniroute") keep working.
@@ -258,6 +275,7 @@ export function resolveOmniRoutePluginOptions(
: DEFAULT_MODEL_CACHE_TTL_MS;
return {
providerId,
omnirouteProviderId,
displayName,
modelCacheTtl,
baseURL: opts?.baseURL,
@@ -265,6 +283,18 @@ export function resolveOmniRoutePluginOptions(
};
}
/**
* Strip a leading "opencode-" prefix (added only for the OC native-adapter
* gate — see `resolveOmniRoutePluginOptions`) so the returned id is safe to
* embed in anything OmniRoute's own server parses. A user-supplied
* `providerId: "opencode-omniroute"` (already prefixed) resolves to the same
* unprefixed "omniroute" as the default, matching `providerId`'s own
* idempotent-prefix handling above.
*/
function trimLeadingOpencodePrefix(rawProviderId: string): string {
return rawProviderId.startsWith("opencode-") ? rawProviderId.slice("opencode-".length) : rawProviderId;
}
/**
* Strict parse of raw plugin options (as received from opencode.json or a
* direct factory call) into the validated `OmniRoutePluginOptions` shape.
@@ -2661,7 +2691,8 @@ export function createOmniRouteProviderHook(
if (canonicalDedup.has(entry.id)) continue;
if (usable && !isUsableRawModelId(entry.id, usable, rawEnrichment)) continue;
const model = mapRawModelToModelV2(entry, {
providerId: resolved.providerId,
// #6859: server-facing id — NOT the OC-gate-prefixed `resolved.providerId`.
providerId: resolved.omnirouteProviderId,
baseURL,
apiFormat: resolved.features?.apiFormat,
});
@@ -2826,7 +2857,8 @@ export function createOmniRouteProviderHook(
const mapped = mapComboToModelV2(
combo,
memberEntries,
resolved.providerId,
// #6859: server-facing id — NOT the OC-gate-prefixed `resolved.providerId`.
resolved.omnirouteProviderId,
baseURL,
features.apiFormat
);
@@ -2845,7 +2877,8 @@ export function createOmniRouteProviderHook(
}
}
const comboKey = buildComboKey(combo, usedComboKeys, resolved.providerId);
// #6859: server-facing key — NOT the OC-gate-prefixed `resolved.providerId`.
const comboKey = buildComboKey(combo, usedComboKeys, resolved.omnirouteProviderId);
// Collision policy: combos win. Warn ONCE per (cacheKey, comboKey)
// when overwriting a same-key raw model so the operator can spot
@@ -2947,7 +2980,8 @@ export function createOmniRouteProviderHook(
},
status: "active",
release_date: "",
providerID: resolved.providerId,
// #6859: server-facing id — NOT the OC-gate-prefixed `resolved.providerId`.
providerID: resolved.omnirouteProviderId,
options: {},
headers: {},
};

View File

@@ -447,14 +447,14 @@ test("models() returns combo entries merged into the map", async () => {
// 3 raw models + 1 combo = 4 entries
assert.equal(Object.keys(out).length, 4);
assert.ok(out["opencode-omniroute/claude-primary"]);
assert.ok(out["opencode-omniroute/claude-secondary"]);
assert.ok(out["opencode-omniroute/gemini-3-flash"]);
assert.ok(out["opencode-omniroute/claude-tier"]);
assert.ok(out["omniroute/claude-primary"]);
assert.ok(out["omniroute/claude-secondary"]);
assert.ok(out["omniroute/gemini-3-flash"]);
assert.ok(out["omniroute/claude-tier"]);
const combo = out["opencode-omniroute/claude-tier"];
const combo = out["omniroute/claude-tier"];
assert.equal(combo.name, "Claude Tier");
assert.equal(combo.providerID, "opencode-omniroute");
assert.equal(combo.providerID, "omniroute");
// LCD over claude-primary (200k, reasoning) + claude-secondary (100k, no reasoning)
assert.equal(combo.limit.context, 100_000);
assert.equal(combo.capabilities.reasoning, false);
@@ -478,11 +478,11 @@ test("models(): combo with unknown member ids degrades to all-false LCD posture"
{ fetcher: modelsFetcher, combosFetcher }
);
const out = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
assert.ok(out["opencode-omniroute/phantom-combo"]);
assert.ok(out["omniroute/phantom-combo"]);
// With zero resolvable members, LCD = all-false (defensive posture).
assert.equal(out["opencode-omniroute/phantom-combo"].capabilities.toolcall, false);
assert.equal(out["opencode-omniroute/phantom-combo"].capabilities.reasoning, false);
assert.equal(out["opencode-omniroute/phantom-combo"].limit.context, 0);
assert.equal(out["omniroute/phantom-combo"].capabilities.toolcall, false);
assert.equal(out["omniroute/phantom-combo"].capabilities.reasoning, false);
assert.equal(out["omniroute/phantom-combo"].limit.context, 0);
});
test("models(): hidden combos are excluded from the map", async () => {
@@ -505,8 +505,8 @@ test("models(): hidden combos are excluded from the map", async () => {
{ fetcher: modelsFetcher, combosFetcher }
);
const out = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
assert.ok(out["opencode-omniroute/visible"]);
assert.ok(!out["opencode-omniroute/hidden"], "hidden combo must be omitted");
assert.ok(out["omniroute/visible"]);
assert.ok(!out["omniroute/hidden"], "hidden combo must be omitted");
});
test("models(): combo name exactly matches raw model id → raw deleted, raw deleted, no warn", async () => {
@@ -530,8 +530,8 @@ test("models(): combo name exactly matches raw model id → raw deleted, raw del
});
// Raw model replaced by combo of the same key; combo now lives at the bare slug.
assert.ok(out["opencode-omniroute/claude-primary"], "combo surfaces under prefixed key");
assert.equal(out["opencode-omniroute/claude-primary"].name, "claude-primary");
assert.ok(out["omniroute/claude-primary"], "combo surfaces under prefixed key");
assert.equal(out["omniroute/claude-primary"].name, "claude-primary");
// No collision warning fires — dedup makes keys disjoint.
const collisionWarns = warnings.filter((w) => {
@@ -565,8 +565,8 @@ test("models(): two combos with same slug → second gets disambiguator suffix",
const out = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
// First combo gets the bare slug; second gets disambiguated.
assert.ok(out["opencode-omniroute/claude"], "first combo at prefixed slug");
assert.ok(out["opencode-omniroute/claude-uuid"], "second combo disambiguated by id prefix");
assert.ok(out["omniroute/claude"], "first combo at prefixed slug");
assert.ok(out["omniroute/claude-uuid"], "second combo disambiguated by id prefix");
});
test("models(): combos fetch fails → falls back to models-only, warn emitted, no throw", async () => {
@@ -583,8 +583,8 @@ test("models(): combos fetch fails → falls back to models-only, warn emitted,
// Catalog includes the models but NOT any combo entries.
assert.equal(Object.keys(out).length, 2);
assert.ok(out["opencode-omniroute/claude-primary"]);
assert.ok(out["opencode-omniroute/claude-secondary"]);
assert.ok(out["omniroute/claude-primary"]);
assert.ok(out["omniroute/claude-secondary"]);
// Soft-fail warning surfaced.
const softFail = warnings.find((w) => {
@@ -609,7 +609,7 @@ test("models(): combos cached + reused within TTL (one combo fetch per TTL windo
const second = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
assert.equal(combosFetcher.callCount(), 1, "combos fetched only once within TTL");
assert.equal(modelsFetcher.callCount(), 1, "models fetched only once within TTL");
assert.ok(second["opencode-omniroute/claude-tier"]);
assert.ok(second["omniroute/claude-tier"]);
});
test("models(): combos refetched after TTL expiry (same key as models)", async () => {
@@ -701,7 +701,7 @@ test("models(): nested combo-ref context is the min of nested + raw members", as
{ fetcher: modelsFetcher, combosFetcher }
);
const out = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
const masterLight = out["opencode-omniroute/master-light"];
const masterLight = out["omniroute/master-light"];
assert.ok(masterLight, "MASTER-LIGHT entry must exist");
assert.equal(
masterLight.limit.context,

View File

@@ -376,7 +376,8 @@ test("provider hook: enrichment fetcher called when features.enrichment !== fals
);
const out = await hook.models!({} as never, { auth: apiAuth("sk") as never });
assert.equal(called, 1, "enrichment fetcher called once");
const m = out["opencode-omniroute/claude-sonnet-4-6"];
// #6859: dynamic-hook catalog keys use the unprefixed omnirouteProviderId.
const m = out["omniroute/claude-sonnet-4-6"];
assert.equal(m.name, "Claude Sonnet 4.6", "enrichment name overlay applied");
assert.equal(m.cost.input, 3, "enrichment pricing applied");
assert.equal(m.cost.output, 15);
@@ -402,7 +403,7 @@ test("provider hook: enrichment fetcher NOT called when features.enrichment:fals
const out = await hook.models!({} as never, { auth: apiAuth("sk") as never });
assert.equal(called, 0, "enrichment fetcher NOT called when gated off");
assert.equal(
out["opencode-omniroute/claude-sonnet-4-6"].name,
out["omniroute/claude-sonnet-4-6"].name,
"claude-sonnet-4-6",
"raw id preserved"
);
@@ -463,7 +464,7 @@ test("provider hook: compression metadata fetcher called when opted in", async (
);
const out = await hook.models!({} as never, { auth: apiAuth("sk") as never });
assert.equal(called, 1, "compression metadata fetcher called");
const combo = out["opencode-omniroute/claude-primary"];
const combo = out["omniroute/claude-primary"];
assert.ok(combo, "combo entry present");
assert.match(
combo.name,

View File

@@ -0,0 +1,99 @@
/**
* Regression test for #6859.
*
* `resolveOmniRoutePluginOptions()` auto-prefixes `providerId` with
* `"opencode-"` (commit 75b52e286) so OpenCode 1.17.8+'s native-adapter gate
* accepts it as an OC-registered provider id. That prefixed value must stay
* OC-internal (AuthHook.provider / provider registration keys) — it must
* NEVER leak into the identifiers OmniRoute's own server parses to resolve
* credentials (`mapRawModelToModelV2`'s `id`/`providerID`,
* `mapComboToModelV2`'s `providerID`, and the dynamic-hook catalog keys).
*
* OmniRoute's server-side `parseModel()` (open-sse/services/model.ts) splits
* a dispatched model string on `/` to recover the provider name and look up
* credentials. If the plugin embeds the OC-gate-prefixed id in that string,
* the server looks up credentials for a provider named "opencode-omniroute"
* (which never exists in `src/shared/constants/providers.ts`) instead of
* "omniroute" — producing the exact "No credentials for opencode-omniroute" /
* "No active credentials for provider: opencode-omniroute" errors reported
* in #6859.
*/
import test from "node:test";
import assert from "node:assert/strict";
import {
createOmniRouteProviderHook,
mapRawModelToModelV2,
resolveOmniRoutePluginOptions,
} from "../src/index.js";
/**
* Minimal stand-in for OmniRoute's own `parseModel()` (open-sse/services/
* model.ts), which splits a dispatched `<providerID>/<modelID>` string on the
* FIRST "/" to recover the provider name used for credential lookup. Kept
* local (rather than cross-importing the real module) so this package's
* self-contained test suite (`cd @omniroute/opencode-plugin && npm test`)
* doesn't depend on the root repo's `@/*` path-alias resolution.
*/
function splitProviderFromDispatchedModel(modelStr: string): string {
const idx = modelStr.indexOf("/");
return idx === -1 ? modelStr : modelStr.slice(0, idx);
}
const apiAuth = (key: string) => ({ type: "api" as const, key });
test("#6859: server-facing model id/providerID must resolve to the unprefixed provider name", () => {
const resolved = resolveOmniRoutePluginOptions();
// The OC-gate-compatible id stays prefixed — it is legitimate for
// AuthHook.provider / provider registration.
assert.equal(resolved.providerId, "opencode-omniroute");
// A second, unprefixed id must be exposed for anything that reaches
// OmniRoute's own server (model id prefix, ModelV2.providerID, combo keys).
assert.equal(
resolved.omnirouteProviderId,
"omniroute",
"resolveOmniRoutePluginOptions() must expose an unprefixed omnirouteProviderId"
);
// A bare raw /v1/models entry (no existing "/" in its id — the common
// case for OmniRoute's catalog) mapped with the server-facing id.
const model = mapRawModelToModelV2(
{ id: "claude-opus-4-7" },
{ providerId: resolved.omnirouteProviderId, baseURL: "http://localhost:20128" }
);
assert.equal(model.providerID, "omniroute");
assert.equal(model.id, "omniroute/claude-opus-4-7");
// OpenCode dispatches back to OmniRoute using `providerID/modelKey`
// (matches the issue's own repro: `-m opencode-omniroute/oc/big-pickle`).
const dispatchedModelString = `${model.providerID}/claude-opus-4-7`;
const parsedProvider = splitProviderFromDispatchedModel(dispatchedModelString);
assert.equal(
parsedProvider,
"omniroute",
`server-side provider split resolved '${parsedProvider}', expected 'omniroute' — ` +
`credentials lookup would fail for an OC-gate-prefixed provider id`
);
});
test("#6859: createOmniRouteProviderHook end-to-end — catalog keys/providerID never carry the OC-gate prefix", async () => {
const hook = createOmniRouteProviderHook(
{ baseURL: "https://or.example.com/v1" },
{
fetcher: async () => [{ id: "claude-opus-4-7" }],
combosFetcher: async () => [],
}
);
const out = await hook.models!({} as never, { auth: apiAuth("sk-test") as never });
const model = out["omniroute/claude-opus-4-7"];
assert.ok(model, "catalog keyed under the unprefixed provider name");
assert.equal(model.providerID, "omniroute");
assert.ok(
!model.providerID.startsWith("opencode-"),
"the OC-gate prefix must never leak into ModelV2.providerID"
);
});

View File

@@ -101,7 +101,10 @@ test("models: extracts apiKey from ctx.auth (type=api) and calls fetcher with it
assert.equal(fetcher.callCount(), 1);
assert.deepEqual(fetcher.callsBy()[0], ["https://or.example.com/v1", "sk-abc"]);
assert.equal(Object.keys(out).length, 3);
assert.ok(out["opencode-omniroute/claude-primary"]);
// #6859: dynamic-hook catalog keys use the unprefixed omnirouteProviderId
// ("omniroute"), not the OC-gate-prefixed hook.id ("opencode-omniroute") —
// that prefix must never leak into anything OmniRoute's server parses.
assert.ok(out["omniroute/claude-primary"]);
});
test("models: returns {} when ctx.auth is null/undefined/wrong-type/empty-key", async () => {
@@ -152,13 +155,17 @@ test("models: maps a sample /v1/models entry to ModelV2 (sanity)", async () => {
{ fetcher, combosFetcher: async () => [] }
);
const out = await hook.models!({} as never, { auth: apiAuth("sk-abc") as never });
const claude = out["opencode-omniroute/claude-primary"];
// #6859: dynamic-hook catalog keys/ids/providerID use the unprefixed
// omnirouteProviderId ("omniroute") — the OC-gate prefix ("opencode-")
// must stay OC-internal (hook.id / AuthHook.provider) and never leak into
// anything OmniRoute's own server parses for credential lookup.
const claude = out["omniroute/claude-primary"];
assert.ok(claude, "claude-primary present");
// `mapRawModelToModelV2` stamps the provider prefix on the id so OC's
// static-catalog reader resolves `(providerID, modelID)` from the key.
assert.equal(claude.id, "opencode-omniroute/claude-primary");
assert.equal(claude.id, "omniroute/claude-primary");
assert.equal(claude.name, "claude-primary");
assert.equal(claude.providerID, "opencode-omniroute");
assert.equal(claude.providerID, "omniroute");
assert.equal(claude.api.id, "openai-compatible");
assert.equal(claude.api.url, "https://or.example.com/v1");
assert.equal(claude.api.npm, "@ai-sdk/openai-compatible");

View File

@@ -3,12 +3,12 @@
## Project
Unified AI proxy/router — route any LLM through one endpoint. Multi-provider support
with **237 provider entries** (OpenAI, Anthropic, Gemini, DeepSeek, Groq, xAI, Mistral, Fireworks,
with **248 provider entries** (OpenAI, Anthropic, Gemini, DeepSeek, Groq, xAI, Mistral, Fireworks,
Cohere, NVIDIA, Cerebras, Pollinations, Puter, Cloudflare AI, HuggingFace, DeepInfra,
SambaNova, Meta Llama API, Moonshot AI, AI21 Labs, Databricks, Snowflake, and many more)
with **MCP Server** (94 tools), **A2A v0.3 Protocol**, and **Electron desktop app**.
> **Live counts (v3.8.43)**: providers 237 · MCP tools 94 · MCP scopes 30 · A2A skills 6 ·
> **Live counts (v3.8.47)**: providers 248 · MCP tools 94 · MCP scopes 30 · A2A skills 6 ·
> open-sse services 134 · routing strategies 17 · auto-combo scoring factors 12 ·
> DB modules 95 · DB migrations 110 · base tables 17 · search providers 11 ·
> i18n locales 42. **Refresh with `npm run check:docs-all`.**
@@ -539,7 +539,7 @@ For any non-trivial change, read the matching deep-dive first:
| Repo navigation | [`docs/architecture/REPOSITORY_MAP.md`](docs/architecture/REPOSITORY_MAP.md) |
| Architecture | [`docs/architecture/ARCHITECTURE.md`](docs/architecture/ARCHITECTURE.md) |
| Engineering reference | [`docs/architecture/CODEBASE_DOCUMENTATION.md`](docs/architecture/CODEBASE_DOCUMENTATION.md) |
| Auto-Combo (12-factor, 17 strategies) | [`docs/routing/AUTO-COMBO.md`](docs/routing/AUTO-COMBO.md) |
| Auto-Combo (12-factor, 18 strategies) | [`docs/routing/AUTO-COMBO.md`](docs/routing/AUTO-COMBO.md) |
| Resilience (3 layers) | [`docs/architecture/RESILIENCE_GUIDE.md`](docs/architecture/RESILIENCE_GUIDE.md) |
| Skills | [`docs/frameworks/SKILLS.md`](docs/frameworks/SKILLS.md) |
| Memory | [`docs/frameworks/MEMORY.md`](docs/frameworks/MEMORY.md) |

View File

@@ -4,6 +4,170 @@
---
## [3.8.48] — TBD
_Development cycle opened at the v3.8.47 freeze (parallel-cycle model). Changes land here via changelog.d fragments._
---
## [3.8.47] — TBD
_Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._
- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten).
### ✨ New Features
- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber)
- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470)
- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625)
- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev)
- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen)
- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7)
- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333)
- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `<alias>/<modelId>` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok)
- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc)
- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp)
- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333)
- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7)
- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`.
- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna)
- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js``pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`.
- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi).
### 🐛 Bug Fixes
- **fix(cli):** the dashboard's Claude Code CLI card could report "Not detected"/"Not installed" even when Claude Code was genuinely installed and previously used ([#6701](https://github.com/diegosouzapw/OmniRoute/issues/6701)) — `getCliRuntimeStatus()` (`src/shared/services/cliRuntime.ts`) determined `installed` purely from binary resolution (known install paths + a `where`/`which` PATH search), with no fallback when that lookup fails for reasons unrelated to whether the CLI is actually installed (stale PATH inherited by a long-running/background process, the binary having moved, an install method not yet catalogued, etc.) — even though `~/.claude/settings.json` on disk proves the tool was installed and used before. Upstream 9router's equivalent route already has this exact fallback. A new `withSettingsFallback()` (`src/shared/services/cliInstallFallback.ts`) restores 9router parity: when the binary lookup's own reason is `"not_found"` (never for deliberate security rejections like unsafe/relative env overrides or symlink escapes) and the tool's settings file exists on disk, `installed` now reports `true`. Regression guard: `tests/unit/repro-6701-claude-detect-fallback.test.ts`.
- **fix(cli):** per-agent AgentBridge DNS toggle was broken for 8 of the 9 supported agents, and a failed MITM startup step could orphan the spawned proxy child — `addDNSEntry`/`removeDNSEntry` (`src/mitm/dns/dnsConfig.ts`) always resolved the legacy Antigravity default hosts regardless of which agent's toggle was flipped, so enabling DNS for Cursor/Codex/Claude Code/etc. silently added only `daily-cloudcode-pa.googleapis.com` while the DB recorded `dns_enabled=true` for the selected agent. Both functions now accept an optional `agentId` and resolve hosts via `ALL_TARGETS`; `POST /api/tools/agent-bridge/agents/[id]/dns` passes the route's `id` through and now returns 404 for an id that doesn't match a known target instead of silently falling back. Separately, `startMitmInternal()` (`src/mitm/manager.ts`) now wraps `generateCert()` (log + rethrow), the `provisionDnsEntries()` call, and the PID-file write in try/catch so a mid-startup failure can't orphan the already-spawned MITM child process. On Windows, `addDNSEntries`/`removeDNSEntries` also batch every missing/present entry into a single elevated PowerShell invocation instead of one UAC prompt per host line. Regression guard: `tests/unit/dns-config-generic.test.ts` (agent-specific resolution + batching), `tests/unit/agent-bridge-dns-route-validation.test.ts` (404 for unknown agent id). ([#6338](https://github.com/diegosouzapw/OmniRoute/pull/6338) — thanks @hamsa0x7)
- **fix(guardrails):** Vision Bridge's individual-model auto-reroute (route an image-bearing request straight to a vision-capable model instead of describe-then-forward) could bypass a policy-restricted API key's model allowlist/budget ([#6640](https://github.com/diegosouzapw/OmniRoute/pull/6640)) — `VisionBridgeGuardrail.preCall()` (`src/lib/guardrails/visionBridge.ts`) swaps `body.model` to the best available vision-capable model, but that swap happens in the guardrail pipeline AFTER `chat.ts` already called `enforceApiKeyPolicy()` against the ORIGINAL model, so a key scoped to a narrow `allowedModels` list could still execute against an unvetted (and possibly costlier) vision model the reroute picked. `chat.ts` now re-validates any guardrail-driven model change against the same per-key allowlist (`isModelAllowedForKey`) before honoring it, falling back to the original already-approved model when the reroute target is not allowed. The reroute path also now honors an explicit `settings.visionBridgeModel` operator override (previously ignored, unlike the combo/describe path a few lines below it, which already respects it via `getVisionBridgeConfig`). Regression guard: `tests/unit/guardrails/visionBridge.test.ts` (22 tests). (thanks @herjarsa)
- **fix(auth):** an API key restricted via `allowedModels`/`allowedCombos` could bypass that restriction entirely over the Codex Responses-over-WebSocket bridge ([#6564](https://github.com/diegosouzapw/OmniRoute/issues/6564)) — `prepare()` in `src/app/api/internal/codex-responses-ws/route.ts` authenticated the WS bridge's API key (`authenticate()`/`authorizeWebSocketHandshake()`) and honored `allowedConnections`, but never called `enforceApiKeyPolicy()`, the same model/combo policy gate the HTTP `/v1/responses` path enforces via `handleChat()` — so a key scoped to e.g. `combo/model-1.0` could still reach a direct Codex model like `gpt-5.5` through this transport, as long as an eligible Codex OAuth connection existed. The bridge's WS auth token arrives via query params (`api_key`/`token`/`access_token`), not a normal `Authorization` header, so a new `enforceCodexWsApiKeyPolicy()` builds an equivalent `Request` carrying an explicit `Authorization: Bearer <apiKey>` header and calls `enforceApiKeyPolicy()` against the CLIENT-requested model, before any Codex-specific model remapping or credential selection. Regression guard: `tests/unit/codex-ws-policy-enforcement-6564.test.ts` (a model-restricted key is rejected 403 before reaching credential selection; a combo-restricted key is rejected 403 requesting a disallowed combo; a key that DOES allow the requested model still proceeds past policy). (thanks @Squawk7777 for the report and an independent fix via #6565)
- **fix(security):** loopback-gate `/api/middleware/*` so a leaked JWT over a tunnel can't install or trigger a middleware hook — middleware hooks compile + run arbitrary JS via `new vm.Script` on the request hot path (`src/lib/middleware/registry.ts`), the same RCE class as the already-gated `/api/plugins/*`; `/api/middleware/` is now in `LOCAL_ONLY_API_PREFIXES` so loopback enforcement runs unconditionally before any auth check (Hard Rules #15 + #17). Regression guard: `tests/unit/route-guard-middleware-local-only.test.ts`. ([#6541](https://github.com/diegosouzapw/OmniRoute/pull/6541)) — see PR. (thanks @developerjillur)
- **fix(startup):** AgentBridge's MITM server no longer fails to start with `ROUTER_API_KEY is required` on a normal install ([#6403](https://github.com/diegosouzapw/OmniRoute/issues/6403)) — `POST /api/tools/agent-bridge/server` resolved the spawned MITM child's router key from only an explicit `apiKey` body field (never sent by the AgentBridge UI — the schema has no such field) and the `ROUTER_API_KEY` env var (unset by default), so `startMitm()` always received `""` and the child hard-exited, even though OmniRoute already had a usable API key in its own DB. A new `resolveRouterApiKey()` now falls back to `pickApiKeyForInternalUse()` (the same DB-backed selector the combo-health-check / cloud-sync internal probes use), resolving in order: explicit key → `ROUTER_API_KEY` env → an existing DB key. Regression guard: `tests/unit/agentbridge-mitm-router-key-6403.test.ts`.
- **fix(providers):** deploying a Cloudflare relay Worker from Dashboard → System → Proxy pool → Cloudflare relay failed immediately with `Cloudflare Worker upload failed: Content-Type must be one of: application/javascript, text/javascript, multipart/form-data`, even with a valid token/account ([#6416](https://github.com/diegosouzapw/OmniRoute/issues/6416)) — the Worker-script upload built a native `FormData` and let `fetch` derive the multipart Content-Type automatically, but in production `globalThis.fetch` is patched with `node_modules/undici`'s own fetch (`open-sse/utils/proxyFetch.ts`), whose `FormData`/`Request` classes differ from the runtime's global `FormData` (same cross-realm class mismatch already fixed once for image edits in #3273); passing a native `FormData` instance through undici's patched fetch made it serialize the body as the literal string `"[object FormData]"` with `Content-Type: text/plain;charset=UTF-8`, which Cloudflare rejects outright. `buildCloudflareWorkerUploadRequest()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now builds the multipart body as a raw `Buffer` with an explicit boundary and `Content-Type: multipart/form-data; boundary=…` header, accepted verbatim by any fetch implementation. Regression guard: `tests/unit/cloudflare-worker-upload-content-type-6416.test.ts` + updated `tests/unit/relay-deploy-5128.test.ts`.
- **fix(security):** SSRF-guard the provider-validation probes so they can no longer be used as an open relay to cloud-metadata endpoints — `directHttpsRequest()` (web-cookie / NVIDIA / Z.AI validation, all with a caller-controllable `baseUrl`) ran with `guard:"none"` + `allowRedirect:true`; it now applies `getProviderValidationGuard()` (default `block-metadata`: LAN/localhost allowed, `169.254.169.254`/link-local IMDS rejected, opt-out via `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS`) and `allowRedirect:false` so a provider can't 3xx-redirect the probe to metadata past the initial-URL guard. Regression guard: `tests/unit/provider-validation-ssrf-guard.test.ts`. ([#6542](https://github.com/diegosouzapw/OmniRoute/pull/6542)) — see PR. (thanks @developerjillur)
- **fix(startup):** AgentBridge's MITM proxy served a mismatched cert for 3 of the 4 antigravity/cloudcode-pa hosts it terminates TLS for, breaking interception ([#6494](https://github.com/diegosouzapw/OmniRoute/issues/6494)) — `src/mitm/server.cjs`'s `TARGET_HOSTS` decrypts all 4 hosts locally (`daily-cloudcode-pa.googleapis.com`, `cloudcode-pa.googleapis.com`, `daily-cloudcode-pa.sandbox.googleapis.com`, `autopush-cloudcode-pa.sandbox.googleapis.com`), but `src/mitm/cert/generate.ts`'s self-signed cert only carried a SAN entry for the first host — a request to any of the other 3 got served a cert whose CN/SAN didn't match (confirmed via `curl -k https://cloudcode-pa.googleapis.com/` showing `CN=daily-cloudcode-pa.googleapis.com`). `generateCert()` now sources its host list from `ANTIGRAVITY_TARGET.hosts` (`src/mitm/targets/antigravity.ts`, the single authoritative registry already kept in lock-step with `server.cjs`/`dnsConfig.ts`/`mitmToolHosts.ts` by their own drift tests) and emits a SAN entry for all 4 hosts instead of hard-coding a second, incomplete copy. Regression guard: `tests/unit/agentbridge-antigravity-cert-hosts-6494.test.ts` (asserts the host list covers all 4 hosts and that the real generated cert's SAN includes each one).
- **fix(resilience):** a `priority` combo never fell back when a target masked credit/quota exhaustion behind an HTTP 200 ([#6427](https://github.com/diegosouzapw/OmniRoute/issues/6427)) — `validateResponseQuality()` (`open-sse/services/combo/validateQuality.ts`) only inspected the response body's top-level `error` field when `choices` was ALSO missing/empty (the narrower #3424 empty-completion case); a masked 200 that echoed a non-empty stub `choices` alongside a structured error object, or a known exhaustion phrase (e.g. "insufficient credits", "quota exceeded") in the error envelope, slipped through as "valid" and the combo kept returning the dead target's response forever instead of failing over. The quality check now inspects the error envelope — a top-level OpenAI-shape `error` object, or a bounded, case-insensitive exhaustion-phrase match against `error.message`/`error.code`/`error.type`/top-level `message`/`detail` — unconditionally, before any shape-specific branch, and regardless of whether `choices`/`output` also look structurally present. The check never inspects `choices[].message.content`, so a legitimate completion that merely mentions "quota" or "credits" in assistant prose is not misclassified. Regression guard: `tests/unit/masked-200-exhaustion-fallback-6427.test.ts`.
- **fix(security):** fail-closed CORS for the cookie/session-authed cloud-agent management routes — `getCloudAgentCorsHeaders()` reflected any caller's `Origin` and paired it with `Allow-Credentials: true` (a CSRF/exfil hole); it now defers to the central allowlist (`resolveAllowedOrigin`), echoes only an allowlisted origin with `Vary: Origin`, and emits `Allow-Credentials` only for an explicitly allowlisted origin — never for a `CORS_ALLOW_ALL` wildcard echo. Regression guard: `tests/unit/cloud-agent-cors-failclosed.test.ts`. ([#6543](https://github.com/diegosouzapw/OmniRoute/pull/6543)) — see PR. (thanks @developerjillur)
- **fix(compression):** adaptive-compression ladder ranked 6 real catalog engines (`ccr`, `ionizer`, `relevance`, `llmlingua`, `llm`, `read-lifecycle`) as if they didn't exist ([#6533](https://github.com/diegosouzapw/OmniRoute/issues/6533)) — `ladder.ts`'s `AGGRESSIVENESS` and `REDUCTION_FACTOR` maps only covered the 7 engines wired into `DEFAULT_LADDER` (session-dedup/rtk/headroom/lite/caveman/aggressive/ultra); every other engine registered in `open-sse/services/compression/engines/index.ts` — including `ccr`/`llmlingua`, which the ladder doc comment already says are intentionally addable via `ladderOverride` — fell through to `aggressivenessOf()`'s `?? 0` default (same rank as `"off"`) and `expectedReductionFactor()`'s generic `?? 0.9` fallback, so `floor`-mode escalation could not rank or escalate past them once added to a custom ladder. Both maps now carry entries for all 6 missing real engines, rescaled ×10 (`off:0``ultra:70`) and placed by each engine's documented `stackPriority` (`ionizer` between `rtk`/`headroom`, `relevance` before `caveman`, `llmlingua`/`llm` between `aggressive` and `ultra`, etc.); `mcpAccessibility` — named in the report — is not a registered `CompressionEngine` (it's a separate MCP tool-response truncation mechanism) and was correctly left out. Regression guard: `tests/unit/ladder-engine-maps-6533.test.ts` (asserts every id from `listCompressionEngines()` ranks above `"off"` with a non-default reduction factor). (thanks @chirag127)
- **fix(api):** tool-call arguments could render as `[object Object]` sequences instead of the real JSON through the `/anthropic` (Anthropic-shape `/messages`) routing path ([#6459](https://github.com/diegosouzapw/OmniRoute/issues/6459)) — `appendToolCallArgumentDelta()` (`open-sse/utils/toolCallArguments.ts`), the shared accumulator the streaming `openai-to-claude` response translator, `openai-responses` translator, and `responsesTransformer` all call to build up a tool call's `arguments`/`input_json_delta` buffer, treated any non-string `incoming` fragment as an empty string. Some upstreams deliver the full `tool_calls[].function.arguments` value as an already-parsed JSON object/array instead of the OpenAI-contracted JSON-encoded string; the old code silently discarded that fragment, leaving `tool_use.input` empty, and left downstream buffers open to a plain string coercion of the object (`[object Object]`) once client-side concatenation kicked in. `appendToolCallArgumentDelta()` now `JSON.stringify()`s a non-string, non-null object/array fragment into a valid JSON fragment instead of dropping it, so the assembled `partial_json` always parses back into the original structured value. Regression guard: `tests/unit/anthropic-toolcall-args-6459.test.ts`. (thanks @chirag127)
- **fix(providers):** `fusion` combo returned the opaque `"All fusion panel models failed"` 503 even when only a minority of panel members were actually cooling down / rate-limited, and a user-supplied `fusionTuning.minPanel=1` was silently overridden ([#6454](https://github.com/diegosouzapw/OmniRoute/issues/6454)) — `handleFusionChat()` hard-clamped the quorum floor via `Math.min(Math.max(2, cfg.minPanel), panel.length)`, so an operator-configured `minPanel=1` never took effect: `collectPanel()`'s straggler-grace timer only starts once `ok >= minPanel`, and with the floor forced to 2 a single fast success plus N slow-failing stragglers never reached quorum, so the panel sat waiting instead of degrading to the survivor. Per-member failure reasons (`straggler_dropped`/`timeout`/`threw`/`status_XXX`/`empty_content`/`unparseable`) were also logged server-side but never surfaced in the 503 body, leaving operators unable to tell a rate-limit fan-fail from a broader outage. Fixed by honoring `Math.max(1, cfg.minPanel)` and threading a `failures: Array<{ model, reason }>` collector into the 503 message (`model=reason` per entry) — production fix already merged via #6521; this entry backfills the missing CHANGELOG bullet and adds an 11-member, `fusion-free`-scale regression test matching the original repro shape (a cooling minority must not sink a healthy majority; a genuinely all-failed panel still returns the documented 503). Regression guard: `tests/unit/services/fusion-min-panel-and-failure-detail.test.ts` + `tests/unit/fusion-partial-panel-failure-6454.test.ts`. (thanks @chirag127)
- **fix(providers):** `fusion` combo strategy silently returned a panel member's raw answer instead of the configured `config.judgeModel` synthesis ([#6455](https://github.com/diegosouzapw/OmniRoute/issues/6455)) — `handleFusionChat()`'s single-survivor "degrade gracefully" path (added for #6454) returned the lone panel answer directly whenever only one panelist succeeded, regardless of whether an explicit `judgeModel` was configured; with the default `minPanel: 2` and a 2-model panel, any single flaky/rate-limited panelist forced this path on every request, so the configured judge (e.g. `auto/claude-opus`) was never invoked and the client-visible `.model` reflected whichever panelist happened to survive. The judge is now still invoked to synthesize a lone surviving answer whenever `judgeModel` is explicitly configured; the cheap direct-answer shortcut is kept only for the implicit case (no `judgeModel` set, where the "judge" is just `panel[0]`). Regression guard: `tests/unit/fusion-judge-model-6455.test.ts` + updated `tests/unit/combo-fusion-strategy.test.ts`. (thanks @chirag127)
- **feat(combo):** sanitized diagnostic trace on an auto-combo terminal failure — instead of an opaque 503, a terminal combo failure now returns a whitelist-projected trace (candidate pool size, attempted count, excluded provider/reason codes, attempt order, and a terminal-reason code) via the new `errorResponseWithComboDiagnostics()`/`sanitizeComboDiagnostics()` in `open-sse/utils/error.ts` — provider/model ids and enumerated reason codes only, never keys/tokens/bodies, length- and count-capped. A reasoning-budget-exhausted panel now returns an actionable "increase max_tokens" message rather than a blind retry-limit 503. Regression guard: `tests/unit/combo-diagnostics-trace.test.ts`. ([#6545](https://github.com/diegosouzapw/OmniRoute/pull/6545)) — see PR. (thanks @developerjillur)
- **fix(providers):** image/diffusion models discovered from an upstream catalog (e.g. HuggingFace's live `/v1/models`) are no longer advertised as chat models ([#6457](https://github.com/diegosouzapw/OmniRoute/issues/6457)) — the chat catalog builder defaulted synced models with no modality info to `endpoints: ["chat"]`, so `huggingface/stabilityai/stable-diffusion-xl-base-1.0` showed up in the chat `/v1/models` listing and returned `400 "not a chat model"` when called. `catalog.ts` now skips any synced model already registered as an image model for that provider (via the new `isRegisteredImageModel()`), leaving `getAllImageModels()` to list it with the correct `type: "image"`. Regression guard: `tests/unit/image-model-not-in-chat-catalog-6457.test.ts`.
- **fix(resilience):** combo session stickiness never released a pin on a credits-exhausted/banned/expired account, permanently defeating failover for that conversation ([#6692](https://github.com/diegosouzapw/OmniRoute/issues/6692)) — `applySessionStickiness()` (`open-sse/services/combo/sessionStickiness.ts`) gated the sticky pin only on 5h/weekly usage-percentage headroom, which is orthogonal to account availability: a `credits_exhausted`/`banned`/`expired` connection, or one still inside its `rateLimitedUntil` cooldown, reports perfectly healthy headroom, so the pin was force-promoted back to the front of the target list on every subsequent turn. `clearStickyBinding()` also had zero call sites in `combo.ts`'s failure paths, so a quality-validation-rejected 200 (a masked daily-cap refusal) never released the pin either. The gate now also resolves the bound connection's terminal status/cooldown via a new injectable fetcher seam (fail-open on lookup errors, mirroring the existing saturation fetcher), and `combo.ts`'s two dispatchers (`handleComboChat`/`handleRoundRobinCombo`) release the pin immediately at both their connection-exhaustion classification point and their quality-validation-failure branch via the new `releaseStickyPinOnFailure()`. Regression guard: `tests/unit/repro-6692-sticky-terminal.test.ts` + extended `tests/unit/combo-session-stickiness.test.ts`.
- **fix(test):** replace the bare `expect(true).toBe(true)` tautology in `playground-api-tab.test.tsx`'s SSE test and close the `check:test-masking` gap that let it slip through for a full cycle ([#6404](https://github.com/diegosouzapw/OmniRoute/issues/6404)) — a prior pass (#6548) had already swapped the literal to `expect(sendBtn).toBeDefined()`, but that stayed just as vacuous: the test's fetch mock returned an empty `/v1/models` list, so `ApiTab`'s Send button is always `disabled` (`!selectedModel`) and the SSE branch never runs — the "SSE infra is verified" comment was never true. The test now mocks a real model, drives the model `<select>` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127)
- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`.
- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur)
- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog)
- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150`freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer)
- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur)
- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`.
- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`.
- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur)
- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`.
- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`.
- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`.
- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`.
- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`.
- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`.
- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`.
- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`.
- **fix(api):** `POST /api/keys` no longer hangs 2090+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting).
- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request").
- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz)
- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot)
- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77)
- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`.
- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild <pkg>` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br)
- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen)
- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape)
- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn)
### 📝 Maintenance
- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari)
- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari)
- **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases).
- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11.
### 🙌 Contributors
Thanks to everyone whose work landed in v3.8.47:
| Contributor | PRs / Issues |
| -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 |
| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 |
| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 |
| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report |
| [@anki1kr](https://github.com/anki1kr) | #6041, #6078 |
| [@arssnndr](https://github.com/arssnndr) | #6163 |
| [@artickc](https://github.com/artickc) | #6363, #6763 |
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248, #6280, #6331, #6675 |
| [@charleszolot](https://github.com/charleszolot) | #6571 |
| [@chirag127](https://github.com/chirag127) | #6145, #6189, #6265, #6328, #6400, #6402, #6404, #6405, #6406, #6407, #6408, #6412, #6414, #6513, #6515, #6516, #6517, #6519, #6521, #6523, #6525, #6526, #6532, #6534, #6546, #6547, #6548, #6549, #6550, #6551, #6552, #6577, #6643, #6644, #6645, #6646, #6703, #6756, #6757, #6769, #6804 |
| [@chy1211](https://github.com/chy1211) | direct commit / report |
| [@developerjillur](https://github.com/developerjillur) | #6451, #6452, #6541, #6542, #6543, #6545, #6553, #6554, #6558 |
| [@dilneiss](https://github.com/dilneiss) | #6499 |
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193, #6292 |
| [@dtybnrj](https://github.com/dtybnrj) | #6349 |
| [@eidoog](https://github.com/eidoog) | direct commit / report |
| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 |
| [@hajilok](https://github.com/hajilok) | #6126 |
| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 |
| [@hao3039032](https://github.com/hao3039032) | #6351 |
| [@hartmark](https://github.com/hartmark) | #6216 |
| [@herjarsa](https://github.com/herjarsa) | #6640 |
| [@Iammilansoni](https://github.com/Iammilansoni) | #6200, #6209, #6245, #6366 |
| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 |
| [@itiwant](https://github.com/itiwant) | direct commit / report |
| [@janeza2](https://github.com/janeza2) | #6308 |
| [@jmengit](https://github.com/jmengit) | #6372, #6443 |
| [@jordansilly77-stack](https://github.com/jordansilly77-stack) | #6316 |
| [@JxnLexn](https://github.com/JxnLexn) | #6335, #6361 |
| [@kanztu](https://github.com/kanztu) | #6181 |
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
| [@KooshaPari](https://github.com/KooshaPari) | #6144, #6166, #6173, #6257, #6611, #6632 |
| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report |
| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report |
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
| [@makcimbx](https://github.com/makcimbx) | #6303 |
| [@MikeTuev](https://github.com/MikeTuev) | #6586 |
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186, #6294, #6728 |
| [@nowhats-br](https://github.com/nowhats-br) | #6700 |
| [@oyi77](https://github.com/oyi77) | #6309 |
| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 |
| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 |
| [@rafpigna](https://github.com/rafpigna) | #6574 |
| [@rianonehub](https://github.com/rianonehub) | #6204 |
| [@ryanngit](https://github.com/ryanngit) | direct commit / report |
| [@samimozcan](https://github.com/samimozcan) | #6753, #6762 |
| [@samir-abis](https://github.com/samir-abis) | direct commit / report |
| [@SeaXen](https://github.com/SeaXen) | #6496, #6678 |
| [@serverless83](https://github.com/serverless83) | #6212 |
| [@shabeer](https://github.com/shabeer) | direct commit / report |
| [@Squawk7777](https://github.com/Squawk7777) | #6565 |
| [@strangersp](https://github.com/strangersp) | #6587 |
| [@swingtempo](https://github.com/swingtempo) | #6312 |
| [@Theadd](https://github.com/Theadd) | #6195 |
| [@Thinkscape](https://github.com/Thinkscape) | #6635 |
| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 |
| [@tjengbudi](https://github.com/tjengbudi) | #4009 |
| [@vinayakkulkarni](https://github.com/vinayakkulkarni) | #6324, #6332 |
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
| [@whale9820](https://github.com/whale9820) | direct commit / report |
| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 |
| [@xz-dev](https://github.com/xz-dev) | #6322, #6323, #6330, #6336, #6702, #6727 |
| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 |
---
### 🐛 Bug Fixes
- **fix(i18n):** the provider-detail (`/dashboard/providers/[id]`) visibility + free/paid model filter labels (`showVisibleOnly`, `showHiddenOnly`, `freeFilterAll`, `freeFilterFreeOnly`, `freeFilterPaidOnly`, `hideAllModels`, plus the currently-unused `filterVisible`/`filterHidden`/`filterByVisibility`) rendered as the literal `__MISSING__:<english>` sentinel in 15 locales, including pt-BR ([#6694](https://github.com/diegosouzapw/OmniRoute/issues/6694)) — `providerText()` (`providerPageHelpers.ts`) checks `t.has(key)` before falling back to clean English, and `t.has()` returns `true` even when the stored value is the `__MISSING__:` sentinel `scripts/i18n/sync-ui-keys.mjs` writes when mirroring keys across locales, so the sentinel rendered verbatim instead of the fallback. Disjoint key set from #6290 (`filterAll`/`filterActive`/`filterError`/`filterBanned`/`filterCreditsExhausted`). All 9 keys now carry real translations across the 15 affected locale files (`it`, `ja`, `ko`, `mr`, `ms`, `nl`, `no`, `phi`, `pl`, `pt`, `pt-BR`, `ro`, `ru`, `sk`, `sv`). Regression guard: `tests/unit/i18n-provider-visibility-filter-keys-6694.test.ts`.
---
## [3.8.46] — 2026-07-07
### ✨ New Features
@@ -3213,6 +3377,7 @@ And thank you to the OmniRoute community for the bug reports, reproductions, and
### Fixed
- **usage:** use xAI's exact provider-reported cost when present instead of always estimating from token counts. (thanks @ryanngit)
- **memory:** the `recent` retrieval strategy no longer drops recent memories whose
text doesn't overlap the current prompt. It was internally mapped to the `exact`
path, which relevance-filtered by the forwarded prompt (`score > 0`), so

View File

@@ -35,7 +35,7 @@ For full test matrix, see `CONTRIBUTING.md` → "Running Tests". For deep archit
## Project at a Glance
**OmniRoute** — unified AI proxy/router. One endpoint, 237 LLM providers, auto-fallback.
**OmniRoute** — unified AI proxy/router. One endpoint, 248 LLM providers, auto-fallback.
| Layer | Location | Purpose |
| ------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
@@ -72,7 +72,7 @@ Client → /v1/chat/completions (Next.js route)
API routes follow a consistent pattern: `Route → CORS preflight → Zod body validation → Optional auth (extractApiKey/isValidApiKey) → API key policy enforcement → Handler delegation (open-sse)`. No global Next.js middleware — interception is route-specific.
**Combo routing** (`open-sse/services/combo.ts`): 18 strategies (priority, weighted, fill-first, round-robin, P2C, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline). Each target calls `handleSingleModel()` which wraps `handleChatCore()` with per-target error handling and circuit breaker checks. The `fusion` strategy is the exception: it fans out to a panel of models in parallel, then a judge model synthesizes one final answer (`open-sse/services/fusion.ts`). See `docs/routing/AUTO-COMBO.md` for the 12-factor Auto-Combo scoring + the full strategy table and `docs/architecture/RESILIENCE_GUIDE.md` for the 3 resilience layers.
**Combo routing** (`open-sse/services/combo.ts`): 18 strategies (priority, weighted, fill-first, round-robin, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline). Each target calls `handleSingleModel()` which wraps `handleChatCore()` with per-target error handling and circuit breaker checks. The `fusion` strategy is the exception: it fans out to a panel of models in parallel, then a judge model synthesizes one final answer (`open-sse/services/fusion.ts`). See `docs/routing/AUTO-COMBO.md` for the 12-factor Auto-Combo scoring + the full strategy table and `docs/architecture/RESILIENCE_GUIDE.md` for the 3 resilience layers.
---
@@ -428,8 +428,10 @@ git push -u origin feat/your-feature
**Husky hooks**:
- **pre-commit**: lint-staged + `check-docs-sync` + `check:any-budget:t11`
- **pre-push**: fast deterministic gates (`check:any-budget:t11` + `check:tracked-artifacts`); intentionally excludes `test:unit` (slow — covered by the CI `test-unit` job). Activated 2026-06-13 (Quality Gates Fase 6A.12).
- **pre-commit**: lint-staged + `check-docs-sync` + `check:any-budget:t11` + `check:tracked-artifacts`
- **pre-push**: intentionally light (PATH/npm sanity only). `any-budget` + `tracked-artifacts`
already run on pre-commit; re-running them on every push was pure double-pay. CI still
enforces both. (Was Fase 6A.12 full pre-push gate; folded into pre-commit in #6716.)
### Worktree isolation (MANDATORY for every development task)
@@ -540,7 +542,7 @@ the stale-enforcement added in Fase 6A.3.
18. Every bug fix must be validated before shipping: a failing-then-passing unit/integration test (TDD) OR a documented live test on the production VPS (192.168.0.15). A fix without either is not merged. See Testing → "Bug fix / issue triage protocol" for the full decision tree.
19. Never develop on the shared main checkout. Every development task runs in its own git worktree on its own dedicated branch, and you MUST confirm the base branch with the operator (e.g. via `AskUserQuestion`) before creating the worktree/branch — never assume `main` or the currently checked-out branch. A `git checkout` in the shared checkout silently destroys other sessions' uncommitted work. Tear down only the worktrees/branches you created (by name, never `fix/*`/`feat/*` wildcards), leave other sessions' worktrees untouched, and end on the branch you started on (the active `release/vX.Y.Z`, never `main`). See Git Workflow → "Worktree isolation".
20. PII redaction/sanitization is **opt-in — never on by default**. OmniRoute proxies for self-hosted/local LLMs where the operator owns the data, so mutating request/response payloads by default would silently corrupt legitimate traffic. The two data-mutating PII feature flags **MUST** keep `defaultValue: "false"` in `src/shared/constants/featureFlagDefinitions.ts`: `PII_REDACTION_ENABLED` (request-side) and `PII_RESPONSE_SANITIZATION` (response + streaming). All three application points — `src/lib/guardrails/piiMasker.ts` (request guardrail), `src/lib/piiSanitizer.ts` (response), `src/lib/streamingPiiTransform.ts` (SSE) — are gated on these flags; with both off the `pii-masker` guardrail still runs but never mutates payloads (data passes through untouched). Flipping either default to `"true"` requires explicit operator approval. The regression guard is `tests/unit/pii-opt-in-default.test.ts` (asserts both definition defaults + behavioral pass-through). Opt-in is per-operator via env or the settings/DB override (`src/lib/db/featureFlags.ts`), never a silent default. See `docs/security/GUARDRAILS.md`.
21. **Release-freeze — the FROZEN release branch belongs to the release captain; development does NOT stop (parallel-cycle model, 2026-07-04).** `/generate-release` opens a marker issue labeled `release-freeze` at the start of reconciliation (Phase 0a), **immediately cuts the next cycle's branch `release/vX+1` from the frozen tip (Phase 0a.0b — bump + living release PR + re-home of open PRs)**, and closes the freeze once the release PR squash-merges to `main`. Before merging **any** PR, every campaign workflow (`/review-issues`, `/review-prs`, `/implement-features`, `/green-prs`, `/port-upstream-*`) **MUST** check `gh issue list --repo diegosouzapw/OmniRoute --label release-freeze --state open` — if a freeze is active: **NEVER merge into the frozen `release/vX.Y.Z` named in the freeze title**; instead resolve the ACTIVE development branch (the **highest** `release/v*` by semver — normally `release/vX+1`, announced in a freeze-issue comment) and **retarget the PR there** (`gh pr edit <N> --base release/vX+1`, then VERIFY with `gh pr view <N> --json baseRefName` — the edit fails silently) and merge normally. **HOLD only when the highest release/v\* branch IS the frozen one** (the short window before 0a.0b completes, or a pre-parallel-cycle release) — in that case leave the PR ready and open, tell the operator, and resume when the next branch appears or the freeze lifts. The just-shipped fixes reach `release/vX+1` via the Phase 5 sync-back (`scripts/release/sync-next-cycle.mjs`); do not try to sync mid-release. This is a **coordination signal, not a permission lock**: the release captain and the campaign sessions share the `diegosouzapw` identity, so a GitHub branch-protection lock cannot distinguish them — only this honored marker prevents the mid-release commit races that forced full CHANGELOG re-reconciliation in v3.8.40/v3.8.41 (a parallel campaign advanced `release/vX.Y.Z` by 34 commits mid-run). The release captain's own reconciliation/cycle-open pushes are exempt — they _are_ the release. Fixes that must land during a freeze (a homologation finding) follow the post-merge read-only rule: land on `main` first via `fix/release-vX.Y.Z-*`. **⛔ ONLY `/generate-release` may raise a release-freeze, and ONLY at its Phase 0a (start of generating a new version) — lifted at Phase 12c after the squash-merge to `main`.** No campaign, session, or agent may open a `release-freeze` marker at any other time — a freeze is **never** a mid-development coordination tool. If a session ever believes a freeze is genuinely, unavoidably necessary outside the `/generate-release` flow, it **MUST first ask the operator (`diegosouzapw`) in chat, explicitly alert "estou criando um freeze" and get an explicit yes** — never open, extend, or re-open a `release-freeze` autonomously. Conversely, do **not** close/lift an active `/generate-release` freeze to unblock campaign merges: it protects the captain's single clean CI run and auto-lifts at Phase 12c — closing it early re-triggers the exact commit race it prevents. Verify a freeze is legitimate before acting on it: an open `release-freeze` whose title/body references an **OPEN** release PR (`gh pr view <N> --json state`) is the authorized captain freeze — hold, don't touch.
21. **Release-freeze — the FROZEN release branch belongs to the release captain; development does NOT stop (parallel-cycle model, 2026-07-04).** `/generate-release` opens a marker issue labeled `release-freeze` at the start of reconciliation (Phase 0a), **immediately cuts the next cycle's branch `release/vX+1` from the frozen tip (Phase 0a.0b — bump + living release PR + re-home of open PRs)**, and closes the freeze once the release PR squash-merges to `main`. Before merging **any** PR, every campaign workflow (`/review-prs`, `/review-group-prs`, `/merge-prs`, `/triage-fix-bugs`, `/implement-fix-bugs`, `/triage-features`, `/implement-features`, `/green-prs`, `/port-upstream-*`) **MUST** check `gh issue list --repo diegosouzapw/OmniRoute --label release-freeze --state open` — if a freeze is active: **NEVER merge into the frozen `release/vX.Y.Z` named in the freeze title**; instead resolve the ACTIVE development branch (the **highest** `release/v*` by semver — normally `release/vX+1`, announced in a freeze-issue comment) and **retarget the PR there** (`gh pr edit <N> --base release/vX+1`, then VERIFY with `gh pr view <N> --json baseRefName` — the edit fails silently) and merge normally. **HOLD only when the highest release/v\* branch IS the frozen one** (the short window before 0a.0b completes, or a pre-parallel-cycle release) — in that case leave the PR ready and open, tell the operator, and resume when the next branch appears or the freeze lifts. The just-shipped fixes reach `release/vX+1` via the Phase 5 sync-back (`scripts/release/sync-next-cycle.mjs`); do not try to sync mid-release. This is a **coordination signal, not a permission lock**: the release captain and the campaign sessions share the `diegosouzapw` identity, so a GitHub branch-protection lock cannot distinguish them — only this honored marker prevents the mid-release commit races that forced full CHANGELOG re-reconciliation in v3.8.40/v3.8.41 (a parallel campaign advanced `release/vX.Y.Z` by 34 commits mid-run). The release captain's own reconciliation/cycle-open pushes are exempt — they _are_ the release. Fixes that must land during a freeze (a homologation finding) follow the post-merge read-only rule: land on `main` first via `fix/release-vX.Y.Z-*`. **⛔ ONLY `/generate-release` may raise a release-freeze, and ONLY at its Phase 0a (start of generating a new version) — lifted at Phase 12c after the squash-merge to `main`.** No campaign, session, or agent may open a `release-freeze` marker at any other time — a freeze is **never** a mid-development coordination tool. If a session ever believes a freeze is genuinely, unavoidably necessary outside the `/generate-release` flow, it **MUST first ask the operator (`diegosouzapw`) in chat, explicitly alert "estou criando um freeze" and get an explicit yes** — never open, extend, or re-open a `release-freeze` autonomously. Conversely, do **not** close/lift an active `/generate-release` freeze to unblock campaign merges: it protects the captain's single clean CI run and auto-lifts at Phase 12c — closing it early re-triggers the exact commit race it prevents. Verify a freeze is legitimate before acting on it: an open `release-freeze` whose title/body references an **OPEN** release PR (`gh pr view <N> --json state`) is the authorized captain freeze — hold, don't touch.
22. **Cross-session safety — this repo is worked by MANY parallel sessions/agents at once; never step on another's in-flight work.** Two absolute bans, both recurring incidents (this rule exists because they keep happening):
- **(a) Never `git stash` / `git stash pop` — ANYWHERE in this repo, including inside an isolated worktree, and including inside any subagent you dispatch.** `git stash` operates on the **shared repository object store**, not the per-worktree working tree — so a stash pushed or popped in one session can silently clobber or resurrect another parallel session's uncommitted changes. This is not hypothetical: 2026-07-02 a `#5923` quotaCache change leaked into the unrelated `#2296` worktree via a global `stash pop`, and the same class reincided through a **subagent**. To compare working changes against a base ref **without** stashing, use `git show <ref>:<path>` or `git diff <ref> -- <path>`; to confirm a typecheck/lint error is pre-existing on the base, inspect the base ref directly (`git show origin/release/vX.Y.Z:<path>`) — never stash your tree away to "get it clean". **Put this ban verbatim in the prompt of every subagent that touches git** (agents don't inherit this file's context — the recurrence was a subagent).
- **(b) Never merge, push, rebase, or force-push a PR / branch / worktree that another session is actively working.** An open PR whose head is a live fix worktree in `.claude/worktrees/` you did **not** create (e.g. `fix-5852`/`fix-5923` carrying fresh commits, even when they share your `diegosouzapw` identity), or any branch another session owns, is **off-limits — HOLD**, and let the owning session merge it. **Before** merging or pushing to any PR you did not create _this_ session, run `git worktree list` to check for a matching in-flight worktree and re-check `gh pr view <N> --json state,headRefOid`. Only the owning session merges its own in-flight PR; mid-flight merges race the owner and re-trigger the exact commit/CHANGELOG races Rule #19 and Rule #21 guard against. (Reinforces Rule #19.)

View File

@@ -341,7 +341,7 @@ Write unit tests in `tests/unit/` covering at minimum:
- [ ] Error responses route through `buildErrorBody()` / `sanitizeErrorMessage()` — no raw stack traces in response bodies (see [`docs/security/ERROR_SANITIZATION.md`](./docs/security/ERROR_SANITIZATION.md))
- [ ] Shell commands (`exec` / `spawn`) pass runtime values via `env`, not via string interpolation
- [ ] All inputs validated with Zod schemas
- [ ] CHANGELOG updated (if user-facing change)
- [ ] Changelog **fragment** added under `changelog.d/{features|fixes|maintenance}/<PR>-<slug>.md` for user-facing changes (see [`changelog.d/README.md`](./changelog.d/README.md)) — do **not** edit `CHANGELOG.md` directly; fragments are aggregated at release time and never conflict between PRs
- [ ] Documentation updated (if applicable)
- [ ] No new CodeQL / Secret-Scanning alerts opened, or each one dismissed with technical justification referencing the relevant `docs/security/` doc
- [ ] Routes that spawn child processes (`/api/mcp/`, `/api/cli-tools/runtime/`) classified as `isLocalOnlyPath()` in `src/server/authz/routeGuard.ts` — see [Hard Rule #15](docs/security/ROUTE_GUARD_TIERS.md)

View File

@@ -55,9 +55,16 @@ ENV NPM_CONFIG_LEGACY_PEER_DEPS=true
# are reproducible.
RUN test -f package-lock.json \
|| (echo "package-lock.json is required for reproducible Docker builds" >&2 && exit 1)
# `npm rebuild <pkg>` re-runs the package's own install script, so under npm 11 +
# `--ignore-scripts` on the parent `npm ci` it depends on npm's script-allowlist
# machinery correctly re-enabling that one package's script. Some self-hosted build
# environments (e.g. Dokploy) hit a broken/incomplete better-sqlite3 native binding
# from that indirection. Invoking `node-gyp rebuild` directly inside the package
# directory bypasses npm's script-running layer entirely and is deterministic
# regardless of npm version or ignore-scripts allowlist behavior.
RUN --mount=type=cache,id=npm-cache,target=/root/.npm \
npm ci --no-audit --no-fund --legacy-peer-deps --ignore-scripts \
&& npm rebuild better-sqlite3 \
&& (cd node_modules/better-sqlite3 && npx --yes node-gyp rebuild) \
&& node -e "require('better-sqlite3')(':memory:').close()"
# Build with Turbopack (stable in Next 16, the repo default). The v3.8.27-era

View File

@@ -6,7 +6,7 @@
# 🚀 OmniRoute — The Free AI Gateway
### Never stop coding. Connect every AI tool to **237 providers** — **90+ free** — through one endpoint.
### Never stop coding. Connect every AI tool to **248 providers** — **90+ free** — through one endpoint.
**Plug Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude / GPT / Gemini. Auto-fallback.**
<br/>
@@ -21,16 +21,21 @@
<h3>
⭐ Star the repo if OMNIROUTE helped you save money and make your work easier. [![Stars](https://img.shields.io/github/stars/diegosouzapw/OmniRoute?style=social)](https://github.com/diegosouzapw/OmniRoute)
⭐ Star the repo if OMNIROUTE helped you save money and make your work easier.
</h3>
[![Stars](https://img.shields.io/github/stars/diegosouzapw/OmniRoute?style=social)](https://github.com/diegosouzapw/OmniRoute)
<a href="https://trendshift.io/repositories/23589" target="_blank"><img src="https://trendshift.io/api/badge/repositories/23589" alt="diegosouzapw%2FOmniRoute | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
[![Star History Rank](https://api.star-history.com/badge?repo=diegosouzapw/OmniRoute&theme=dark)](https://www.star-history.com/diegosouzapw/omniroute)
[![237 AI Providers](https://img.shields.io/badge/237-AI_Providers-6C5CE7?style=for-the-badge)](#-237-ai-providers--90-free)
[![90+ Free](https://img.shields.io/badge/90%2B-Free_Tiers-00B894?style=for-the-badge)](#-237-ai-providers--90-free)
</br>
[![248 AI Providers](https://img.shields.io/badge/248-AI_Providers-6C5CE7?style=for-the-badge)](#-248-ai-providers--90-free)
[![90+ Free](https://img.shields.io/badge/90%2B-Free_Tiers-00B894?style=for-the-badge)](#-248-ai-providers--90-free)
[![1.6B Free Tokens/mo](https://img.shields.io/badge/1.6B-Free_Tokens%2Fmo-00B894?style=for-the-badge)](docs/reference/FREE_TIERS.md)
[![Token Savings](https://img.shields.io/badge/up_to_95%25-Token_Savings-E17055?style=for-the-badge)](#%EF%B8%8F-save-1595-tokens--automatically)
[![17 Strategies](https://img.shields.io/badge/17-Routing_Strategies-0984E3?style=for-the-badge)](#-combos--the-flagship)
[![18 Strategies](https://img.shields.io/badge/18-Routing_Strategies-0984E3?style=for-the-badge)](#-combos--the-flagship)
[![$0 to start](https://img.shields.io/badge/%240-To_Start-FDCB6E?style=for-the-badge&logoColor=black)](#-quick-start)
<br/>
@@ -56,7 +61,7 @@
![Docker Pulls](https://img.shields.io/docker/pulls/diegosouzapw/omniroute?label=docker%20pulls&logo=docker&color=2496ED)
![Electron Downloads](https://img.shields.io/github/downloads/diegosouzapw/omniroute/total?style=flat&label=electron%20downloads&logo=electron&color=47848F)
[**🚀 Quick Start**](#-quick-start) • [**🎯 Combos**](#-combos--the-flagship) • [**🌐 Providers**](#-237-ai-providers--90-free) • [**🔌 CLI & MCP**](#-full-cli--a2a--mcp) • [**🗜️ Compression**](#%EF%B8%8F-save-1595-tokens--automatically) • [**🌍 Website**](https://omniroute.online)
[**🚀 Quick Start**](#-quick-start) • [**🎯 Combos**](#-combos--the-flagship) • [**🌐 Providers**](#-248-ai-providers--90-free) • [**🔌 CLI & MCP**](#-full-cli--a2a--mcp) • [**🗜️ Compression**](#%EF%B8%8F-save-1595-tokens--automatically) • [**🌍 Website**](https://omniroute.online)
[💥 The Promise](#-the-promise) • [🤔 Why](#-why-omniroute) • [🏆 What Sets Apart](#-what-sets-omniroute-apart) • [🤖 Compatible CLIs](#-compatible-clis--coding-agents) • [🖥️ Where It Runs](#%EF%B8%8F-where-omniroute-runs--anywhere) • [🔒 Private](#-private--local-first) • [🎬 In Action](#-omniroute-in-action) • [📚 Explore More](#-explore-more) • [📧 Support](#-support--community)
@@ -144,18 +149,18 @@
</div>
> One endpoint. **237 providers.** Never stop building — and let OmniRoute pick the cheapest one that works.
> One endpoint. **248 providers.** Never stop building — and let OmniRoute pick the cheapest one that works.
<table>
<tr>
<td width="33%" valign="top"><b>🚫 Never hit limits</b><br/><sub>Auto-fallback across 237 providers in milliseconds. Quota out? Next provider takes over — zero downtime.</sub></td>
<td width="33%" valign="top"><b>🚫 Never hit limits</b><br/><sub>Auto-fallback across 248 providers in milliseconds. Quota out? Next provider takes over — zero downtime.</sub></td>
<td width="33%" valign="top"><b>💸 Save up to 95% tokens</b><br/><sub>RTK + Caveman stacked compression cuts 1595% of eligible tokens (~89% avg on tool-heavy sessions).</sub></td>
<td width="33%" valign="top"><b>🆓 $0 to start</b><br/><sub>90+ providers with a free tier, 11 free <i>forever</i> (Kiro, Qoder, Pollinations, LongCat…). No card needed.</sub></td>
</tr>
<tr>
<td width="33%" valign="top"><b>🔌 Every tool works</b><br/><sub>24+ coding agents — Claude Code, Codex, Cursor, Cline, Copilot, Antigravity — through one config.</sub></td>
<td width="33%" valign="top"><b>🧩 One endpoint</b><br/><sub>OpenAI ↔ Claude ↔ Gemini ↔ Responses API translation. Point any tool at <code>/v1</code> and it just works.</sub></td>
<td width="33%" valign="top"><b>🛡️ Production-grade</b><br/><sub>Circuit breakers, TLS stealth, MCP (95 tools), A2A, memory, guardrails, evals. 21,000+ tests.</sub></td>
<td width="33%" valign="top"><b>🛡️ Production-grade</b><br/><sub>Circuit breakers, TLS stealth, MCP (94 tools), A2A, memory, guardrails, evals. 21,000+ tests.</sub></td>
</tr>
</table>
@@ -189,7 +194,7 @@
┌──────────────────────────────────────────────────────────┐
│ OmniRoute — Smart Router │
│ RTK + Caveman compression · 17 routing strategies │
│ RTK + Caveman compression · 18 routing strategies │
│ Circuit breakers · TLS stealth · MCP · A2A · Guardrails │
└─────────────────────────┬──────────────────────────────────┘
┌─────────────┬────┴────────┬─────────────┐
@@ -227,9 +232,9 @@ No combo to create. Set your model to `auto` (or a variant) and OmniRoute builds
##
### 🔀 Or build your own — 17 routing strategies
### 🔀 Or build your own — 18 routing strategies
All **17** strategies — mix & match per combo step:
All **18** strategies — mix & match per combo step:
| # | Strategy | What it does |
| --- | ------------------- | ---------------------------------------------------------------- |
@@ -248,10 +253,11 @@ All **17** strategies — mix & match per combo step:
| 13 | `context-relay` | Hand off context across targets for long conversations 🧠 |
| 14 | `context-optimized` | Pick the best fit for the current context size |
| 15 | `lkgp` | Last-Known-Good Path — sticky to the last successful target |
| 16 | `auto` | 9-factor live scoring across every connection 🤖 |
| 16 | `auto` | 12-factor live scoring across every connection 🤖 |
| 17 | `fusion` | Fan out to a panel of models + a judge synthesizes one answer 🧬 |
| 18 | `pipeline` | Chain steps — each target's output feeds the next one 🔗 |
<sub>The Auto-Combo engine scores every candidate on **9 factors** (health, quota, cost, latency, success rate, freshness…) — see [`docs/routing/AUTO-COMBO.md`](docs/routing/AUTO-COMBO.md).</sub>
<sub>The Auto-Combo engine scores every candidate on **12 factors** (health, quota, cost, latency, success rate, freshness…) — see [`docs/routing/AUTO-COMBO.md`](docs/routing/AUTO-COMBO.md).</sub>
##
@@ -308,11 +314,11 @@ Result: 4 layers of fallback = zero downtime
| Feature | OmniRoute | Other routers |
| -------------------------------------- | ------------------------------------------------------------------- | ------------- |
| 🌐 Providers | **237** | 20100 |
| 🌐 Providers | **248** | 20100 |
| 🆓 Free providers | **90+ (11 free forever)** | 15 |
| 🔀 Routing strategies | **17** (priority, weighted, cost-optimized, context-relay, fusion…) | 13 |
| 🔀 Routing strategies | **18** (priority, weighted, cost-optimized, context-relay, fusion…) | 13 |
| 🗜️ Token compression | **RTK + Caveman stacked (1595%)** | None / 2040% |
| 🧰 Built-in MCP server | **95 tools, 3 transports, 30 scopes** | Rare |
| 🧰 Built-in MCP server | **94 tools, 3 transports, 30 scopes** | Rare |
| 🤝 A2A agent protocol | **6 skills, JSON-RPC 2.0** | None |
| 🧠 Memory (FTS5 + vector) | **Yes** | Rare |
| 🛡️ Guardrails (PII, injection, vision) | **Yes** | Rare |
@@ -346,7 +352,7 @@ Result: 4 layers of fallback = zero downtime
- **🛡️ Security** — a prompt-injection guard across every LLM route (backed by a red-team suite), plus a free DuckDuckGo last-resort web search. → [Guardrails](docs/security/GUARDRAILS.md)
- **🖼️ New endpoints** — `/v1/ocr` (Mistral OCR) and `/v1/audio/translations` (Whisper-style audio translation) round out the media API surface. → [API Reference](docs/reference/API_REFERENCE.md)
- **🌍 Deployment & ops** — reverse-proxy `basePath` deployment (`OMNIROUTE_BASE_PATH`, e.g. serving OmniRoute under `/omniroute/`), browser-language auto-detect on first visit, per-API-key device/connection tracking (IP+UA fingerprint, masked, in-memory only), root-less MITM cert trust for user-namespaced containers (`OMNIROUTE_NO_SUDO`), and server-side configured-only / available-only filters on the Free Provider Rankings page. → [Environment](docs/reference/ENVIRONMENT.md)
- **🤝 More providers & agents** — Cursor Cloud Agent (a 4th cloud agent), CodeBuddy CN (`copilot.tencent.com`), a Google Flow video-generation provider, new gateways **DGrid** and **Pioneer AI** (Fastino Labs), inbound **xAI Grok** translators plus **Grok Build (xAI)** with an OAuth import-token flow, GPT-4 / GPT-4o-mini on the GitHub Copilot provider, multi-model **Factory Droid**, **ZenMux Free** (session-cookie free tier), **Alibaba DashScope** text-to-video (`wan2.7-t2v`), a refreshed 237-provider catalog (OrcaRouter, Wafer AI, OpenAdapter, dit.ai, TokenRouter, …), Vertex AI media generation (speech/transcription/music/video), a first-class **Ollama** local-provider card, the **SenseNova** free Token Plan (chat + text-to-image), one-click account import from CLIProxyAPI (`~/.cli-proxy-api/`), **Claude Sonnet 5** wired end-to-end, a new provider wave (**Kenari**, **SumoPod**, **X5Lab**, **Charm Hyper**, **Nube.sh**, **b.ai**, **Qiniu**, **ModelScope**, **Augment/Auggie CLI**, **ClinePass**, NVIDIA NIM image generation), Codex account import from a raw ChatGPT access token, the **Requesty** gateway (BYOK, ~200 free req/day), **Yuanbao (web)** as a cookie-session provider (DeepSeek V3/R1 + Hunyuan), the **Zed** hosted LLM aggregator (OAuth), **Claude 5 Sonnet** on the Claude Web provider, Kiro **adaptive-thinking reasoning** surfaced as `reasoning_content`, and **bulk API-key add for Cloudflare Workers AI**. → [Providers](docs/reference/PROVIDER_REFERENCE.md)
- **🤝 More providers & agents** — Cursor Cloud Agent (a 4th cloud agent), CodeBuddy CN (`copilot.tencent.com`), a Google Flow video-generation provider, new gateways **DGrid** and **Pioneer AI** (Fastino Labs), inbound **xAI Grok** translators plus **Grok Build (xAI)** with an OAuth import-token flow, GPT-4 / GPT-4o-mini on the GitHub Copilot provider, multi-model **Factory Droid**, **ZenMux Free** (session-cookie free tier), **Alibaba DashScope** text-to-video (`wan2.7-t2v`), a refreshed 248-provider catalog (OrcaRouter, Wafer AI, OpenAdapter, dit.ai, TokenRouter, …), Vertex AI media generation (speech/transcription/music/video), a first-class **Ollama** local-provider card, the **SenseNova** free Token Plan (chat + text-to-image), one-click account import from CLIProxyAPI (`~/.cli-proxy-api/`), **Claude Sonnet 5** wired end-to-end, a new provider wave (**Kenari**, **SumoPod**, **X5Lab**, **Charm Hyper**, **Nube.sh**, **b.ai**, **Qiniu**, **ModelScope**, **Augment/Auggie CLI**, **ClinePass**, NVIDIA NIM image generation), Codex account import from a raw ChatGPT access token, the **Requesty** gateway (BYOK, ~200 free req/day), **Yuanbao (web)** as a cookie-session provider (DeepSeek V3/R1 + Hunyuan), the **Zed** hosted LLM aggregator (OAuth), **Claude 5 Sonnet** on the Claude Web provider, Kiro **adaptive-thinking reasoning** surfaced as `reasoning_content`, and **bulk API-key add for Cloudflare Workers AI**. → [Providers](docs/reference/PROVIDER_REFERENCE.md)
- **⚡ Local performance & infra** — a one-click local Redis launcher (`omniroute redis up`, plus a dashboard Redis panel), one-click **Cloudflare Workers** and **Deno Deploy** relay deployers wired into the proxy pool, a relay-backend selector (`OMNIROUTE_RELAY_BACKEND=ts|bifrost|auto`) so `/v1/relay` stays the stable surface while choosing the fastest backend internally, **Bifrost** (Go AI-gateway) and **Mux** (agent-orchestration daemon) promoted to first-class embedded/supervised services alongside 9Router/CLIProxyAPI, and **Webshare** added as a paid fourth source in the free-proxy provider framework. → [Embedded Services](docs/frameworks/EMBEDDED-SERVICES.md)
<br/>
@@ -389,11 +395,11 @@ Result: 4 layers of fallback = zero downtime
<div align="center">
# 🌐 237 AI Providers — 90+ Free
# 🌐 248 AI Providers — 90+ Free
</div>
> The most complete catalog of any open-source router: **237 providers**, **90+ with a free tier**, **11 free forever**.
> The most complete catalog of any open-source router: **248 providers**, **90+ with a free tier**, **11 free forever**.
<div align="center">
@@ -538,7 +544,7 @@ Expose OmniRoute over **MCP** or **A2A** and any capable agent gets the keys to
| Protocol | Endpoint | Use it for |
| ------------------ | ----------------------------------------------- | ------------------------------------------------------ |
| 🧰 **MCP (stdio)** | `omniroute --mcp` | Plug into Claude Desktop, Cursor, any MCP client |
| 🌊 **MCP (HTTP)** | `http://localhost:20128/api/mcp/stream` | Remote MCP — **95 tools**, 30 scopes, full audit trail |
| 🌊 **MCP (HTTP)** | `http://localhost:20128/api/mcp/stream` | Remote MCP — **94 tools**, 30 scopes, full audit trail |
| 📡 **MCP (SSE)** | `http://localhost:20128/api/mcp/sse` | Streaming MCP transport |
| 🤝 **A2A** | `http://localhost:20128/.well-known/agent.json` | Agent-to-agent, **JSON-RPC 2.0** + SSE, 6 skills |
@@ -568,7 +574,7 @@ Engines run in pipeline order; each is independently toggleable and configurable
| 1 | **Session-Dedup** | Drops content repeated across turns (content-addressed, cross-turn) |
| 2 | **CCR** | Archives large blocks behind retrieve markers, fetched on demand |
| 3 | **RTK** | Smart tool-result filtering, dedup & truncation (command-aware) |
| 4 | **Headroom** | Lossless tabular compaction of homogeneous JSON arrays (~30%+) |
| 4 | **Headroom** | Lossless tabular compaction of homogeneous JSON arrays, flat or nested (~30%), via a vendored **GCF** codec (spec v3.2) |
| 5 | **Relevance** | Extractive sentence scoring against the last user query |
| 6 | **Caveman** | Rule-based prose compression (~6575% on output) |
| 7 | **LLMLingua-2** | ML semantic pruning via MobileBERT ONNX — code-safe, async |
@@ -875,9 +881,9 @@ Compression: aggressive (~50%) → double your free quota · Cost: $0/mo
<br/>
**Routing:** 17 strategies · task-aware smart routing · thinking budget controls · wildcard routing · system prompt injection.
**Routing:** 18 strategies · task-aware smart routing · thinking budget controls · wildcard routing · system prompt injection.
**Compatibility:** OpenAI ↔ Claude ↔ Gemini ↔ Responses API · auto OAuth refresh (PKCE, 8 providers) · multi-account round-robin · Batch + Files API · live OpenAPI 3.0.
**Protocols:** MCP (95 tools, 3 transports, 30 scopes) · A2A (JSON-RPC 2.0, SSE, 6 skills) · ACP · cloud agents (Codex, Cursor, Devin, Jules).
**Protocols:** MCP (94 tools, 3 transports, 30 scopes) · A2A (JSON-RPC 2.0, SSE, 6 skills) · ACP · cloud agents (Codex, Cursor, Devin, Jules).
**Plugins:** custom plugin marketplace (system-configured registry URL with SSRF-guarded fetch) · install / enable / disable · Notion + Obsidian knowledge-base integrations (WebDAV file server, vault search, note CRUD).
**Embedded services:** one-click install & lifecycle management of local sidecar services (CLIProxy, NineRouter).
**Quality & Ops:** built-in **Evals** (golden-set: exact/contains/regex/custom) · guardrails (PII, injection, vision) · health dashboard · p50/p95/p99 telemetry · webhooks · compliance audit.
@@ -901,7 +907,7 @@ Compression: aggressive (~50%) → double your free quota · Cost: $0/mo
**Will I be charged by OmniRoute?** No — it's free, open-source software on your machine. You only pay paid providers directly. OmniRoute has no billing system.
**Are FREE providers really unlimited?** Mostly — Qoder, Pollinations, LongCat, and Cloudflare are free with no per-account credit cap. Kiro is free too but capped at ~50 credits/month per account. Stack multiple free providers in a combo and auto-fallback keeps you serving for $0.
**Will compression hurt quality?** No — it only compresses the **input**; code, URLs, JSON are always protected.
**Does it work where AI is blocked?** Yes — 3-level proxy + 1proxy marketplace reach all 237 providers.
**Does it work where AI is blocked?** Yes — 3-level proxy + 1proxy marketplace reach all 248 providers.
📖 [User Guide](docs/guides/USER_GUIDE.md) · [API Reference](docs/reference/API_REFERENCE.md) · [Environment Config](docs/reference/ENVIRONMENT.md)
@@ -1022,7 +1028,7 @@ Compression: aggressive (~50%) → double your free quota · Cost: $0/mo
| [Compression Rules Format](docs/compression/COMPRESSION_RULES_FORMAT.md) | JSON rule-pack schemas for Caveman and RTK filters |
| [Compression Language Packs](docs/compression/COMPRESSION_LANGUAGE_PACKS.md) | Language detection and Caveman rule-pack authoring |
| [Resilience Guide](docs/architecture/RESILIENCE_GUIDE.md) | Circuit breakers, cooldowns, queue, anti-thundering herd, TLS spoofing |
| [Auto-Combo Engine](docs/routing/AUTO-COMBO.md) | 9-factor scoring, mode packs, self-healing |
| [Auto-Combo Engine](docs/routing/AUTO-COMBO.md) | 12-factor scoring, mode packs, self-healing |
| [Proxy Guide](docs/ops/PROXY_GUIDE.md) | 3-level proxy system, 1proxy marketplace, registry CRUD |
| [Free Tiers](docs/reference/FREE_TIERS.md) | 25+ free API providers consolidated directory |
| [Features Gallery](docs/guides/FEATURES.md) | Visual dashboard tour with screenshots |
@@ -1150,14 +1156,13 @@ gh release create v3.8.2 --title "v3.8.2" --generate-notes
## 📊 Stars
<a href="https://www.star-history.com/?repos=diegosouzapw%2Fomniroute&type=date&legend=top-left">
<a href="https://www.star-history.com/?repos=diegosouzapw%2FOmniRoute&type=date&legend=top-left">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=diegosouzapw/omniroute&type=date&theme=dark&legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=diegosouzapw/omniroute&type=date&legend=top-left" />
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=diegosouzapw/omniroute&type=date&legend=top-left" />
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=diegosouzapw/OmniRoute&type=date&theme=dark&legend=top-left&sealed_token=XP_ycEjv7s31p1edvhsMOXry51OWYsUjDRWjflSG7jQKRpO9hPGg7i_EHvwhI6QtrARTMH-YGjJhi8sumRYflEJD0DPlH_MMHjizhBYCX8fbHFrHEiNvVA" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=diegosouzapw/OmniRoute&type=date&legend=top-left&sealed_token=XP_ycEjv7s31p1edvhsMOXry51OWYsUjDRWjflSG7jQKRpO9hPGg7i_EHvwhI6QtrARTMH-YGjJhi8sumRYflEJD0DPlH_MMHjizhBYCX8fbHFrHEiNvVA" />
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=diegosouzapw/OmniRoute&type=date&legend=top-left&sealed_token=XP_ycEjv7s31p1edvhsMOXry51OWYsUjDRWjflSG7jQKRpO9hPGg7i_EHvwhI6QtrARTMH-YGjJhi8sumRYflEJD0DPlH_MMHjizhBYCX8fbHFrHEiNvVA" />
</picture>
</a>
</div>
<br/>
@@ -1211,7 +1216,7 @@ OmniRoute stands on the shoulders of giants. It started as a fork of **[9router]
| Project | ⭐ | How it inspired OmniRoute |
| ---------------------------------------------------------------------------------------------- | ----: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **[TOON](https://github.com/toon-format/toon)** · toon-format | 24.7k | Token-Oriented Object Notation — its columnar, header-plus-rows model shaped our tabular compaction stage. |
| **[GCF Graph Compact Format](https://github.com/blackwell-systems/gcf)** · Blackwell Systems | 14 | Schema-aware "JSON for LLMs" notation — co-inspired our lossless homogeneous-array compaction with `[N rows]` markers. |
| **[GCF Graph Compact Format](https://github.com/blackwell-systems/gcf)** · Blackwell Systems | 14 | First inspired our tabular compaction stage; now its zero-dependency, lossless generic-profile encoder is **vendored directly** as the Headroom codec (MIT, SPDX-marked), current with GCF spec v3.2. |
| **[token-optimizer-mcp](https://github.com/ooples/token-optimizer-mcp)** · ooples | 421 | Brotli/SQLite cache + per-session context-delta — inspired our `session-dedup` engine. |
| **[token-savior](https://github.com/Mibayy/token-savior)** · Mibayy | 1.0k | Bash-output compaction + MCP profiles — inspired our compression bail-out discipline and MCP tool-manifest reduction. |
| **[token-saver](https://github.com/ppgranger/token-saver)** · ppgranger | 110 | Content-aware, per-file-type output compression with failure-aware bail-out — validated our per-type dispatch and minimum-gain skip. |
@@ -1255,6 +1260,12 @@ OmniRoute stands on the shoulders of giants. It started as a fork of **[9router]
| ------------------------------------------------------------------------------------------- | --: | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| **[awesome-secure-defaults](https://github.com/tldrsec/awesome-secure-defaults)** · tldrsec | 708 | A curated list of secure-by-default libraries that guides our security choices (Helmet.js, DOMPurify, ssrf-req-filter, safe-regex, Google Tink). |
### 🧭 Complementary tools
| Project | How it composes with OmniRoute |
| ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **[CodeWebChat](https://github.com/robertpiosik/CodeWebChat)** · robertpiosik | Editor-side companion — VS Code + browser extension that autofills 15+ chatbot web UIs with editor context. Owns the free-web-UI rail alongside OmniRoute's API rail; can point its API mode at OmniRoute. |
## ❤️ Support
OmniRoute is free and open source, built and maintained in the open. If it saves you time or money, consider supporting development:

View File

@@ -15,7 +15,11 @@ function readCache() {
try {
const raw = JSON.parse(readFileSync(cachePath(), "utf8"));
if (raw && typeof raw.ts === "number" && Date.now() - raw.ts < CACHE_TTL_MS) return raw;
} catch {}
} catch (err) {
if (process.env.OMNIROUTE_DEBUG_COMPLETION) {
console.error("[omniroute completion] readCache failed:", err?.message ?? err);
}
}
return null;
}
@@ -41,12 +45,20 @@ async function refreshCache(opts = {}) {
const j = await mr.value.json();
models = (Array.isArray(j) ? j : j.data || []).map((m) => m.id).filter(Boolean);
}
} catch {}
} catch (err) {
if (process.env.OMNIROUTE_DEBUG_COMPLETION) {
console.error("[omniroute completion] refreshCache failed:", err?.message ?? err);
}
}
const data = { combos, providers, models, ts: Date.now() };
try {
mkdirSync(dirname(cachePath()), { recursive: true });
writeFileSync(cachePath(), JSON.stringify(data));
} catch {}
} catch (err) {
if (process.env.OMNIROUTE_DEBUG_COMPLETION) {
console.error("[omniroute completion] writeCache failed:", err?.message ?? err);
}
}
return data;
}

View File

@@ -24,7 +24,7 @@ async function restCompressionStatus() {
const combosBody = combosRes.ok ? await combosRes.json() : { combos: [] };
const analytics = analyticsRes && analyticsRes.ok ? await analyticsRes.json() : null;
return {
engine: settings.engine ?? null,
strategy: settings.defaultMode || "standard",
settings,
combos: combosBody.combos ?? combosBody,
analytics,
@@ -33,7 +33,10 @@ async function restCompressionStatus() {
async function restCompressionConfigure(config) {
const body = { ...config };
if (body.engine) body.engine = normalizeEngine(body.engine);
if (body.strategy) {
body.defaultMode = body.strategy === "caveman" ? "standard" : normalizeEngine(body.strategy);
delete body.strategy;
}
const res = await apiFetch("/api/settings/compression", { method: "PUT", body });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
@@ -43,9 +46,10 @@ async function restCompressionConfigure(config) {
}
async function restSetEngine(name) {
const normalized = normalizeEngine(name);
const res = await apiFetch("/api/settings/compression", {
method: "PUT",
body: { engine: normalizeEngine(name) },
body: { defaultMode: normalized === "caveman" ? "standard" : normalized },
});
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
@@ -103,7 +107,11 @@ export async function runCompressionStatus(opts, cmd) {
export async function runCompressionConfigure(opts, cmd) {
const config = {};
if (opts.engine) config.engine = opts.engine;
// #6571 — both the MCP tool schema (compressionConfigureInput) and
// handleCompressionConfigure expect `strategy`, not `engine`; a non-strict
// MCP schema silently strips an unrecognized `engine` key on the primary
// (MCP-mounted) path, so this must be `strategy` on both paths.
if (opts.engine) config.strategy = normalizeEngine(opts.engine);
if (opts.cavemanAggressiveness !== undefined)
config.caveman = { aggressiveness: opts.cavemanAggressiveness };
if (opts.rtkBudget !== undefined) config.rtk = { tokenBudget: opts.rtkBudget };
@@ -163,7 +171,7 @@ export function registerCompression(program) {
engine.command("set <name>").action(runCompressionEngineSet);
engine.command("get").action(async (opts, cmd) => {
const data = await mcpCall("omniroute_compression_status", {}, restCompressionStatus);
process.stdout.write(`${data.engine ?? "(default)"}\n`);
process.stdout.write(`${data.strategy ?? "(default)"}\n`);
});
const combos = cmp.command("combos").description(t("compression.combos.description"));

View File

@@ -48,7 +48,11 @@ export async function runHealthCommand(opts = {}) {
}
try {
const res = await apiFetch("/api/health", { retry: false, timeout: 5000, acceptNotOk: true });
const res = await apiFetch("/api/monitoring/health", {
retry: false,
timeout: 5000,
acceptNotOk: true,
});
if (!res.ok) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
return 1;
@@ -66,29 +70,22 @@ export async function runHealthCommand(opts = {}) {
if (health.uptime) console.log(t("health.uptime", { uptime: health.uptime }));
if (health.version) console.log(` Version: ${health.version}`);
if (health.requests !== undefined) {
console.log(t("health.requests", { count: health.requests }));
if (health.activeConnections !== undefined) {
console.log(t("health.requests", { count: health.activeConnections }));
}
if (health.breakers && opts.verbose) {
if (health.circuitBreakers && opts.verbose) {
console.log("\n \x1b[1mCircuit Breakers\x1b[0m");
for (const [name, status] of Object.entries(health.breakers)) {
const state =
status.state === "closed" ? "\x1b[32m● closed\x1b[0m" : "\x1b[33m○ open\x1b[0m";
console.log(` ${name.padEnd(20)} ${state}`);
}
const { open = 0, halfOpen = 0, closed = 0 } = health.circuitBreakers;
console.log(` \x1b[32m● closed\x1b[0m ${closed}`);
console.log(` \x1b[33m○ half-open\x1b[0m ${halfOpen}`);
console.log(` \x1b[31m○ open\x1b[0m ${open}`);
}
if (health.cache && opts.verbose) {
console.log("\n \x1b[1mCache\x1b[0m");
console.log(` Semantic hits: ${health.cache.semanticHits || 0}`);
console.log(` Signature hits: ${health.cache.signatureHits || 0}`);
}
if (opts.verbose && health.memory) {
if (opts.verbose && health.memoryUsage) {
console.log("\n \x1b[1mMemory\x1b[0m");
console.log(` RSS: ${health.memory.rss || "N/A"}`);
console.log(` Heap used: ${health.memory.heapUsed || "N/A"}`);
console.log(` RSS: ${health.memoryUsage.rss || "N/A"}`);
console.log(` Heap used: ${health.memoryUsage.heapUsed || "N/A"}`);
}
return 0;
@@ -100,13 +97,17 @@ export async function runHealthCommand(opts = {}) {
export async function runHealthComponentsCommand(opts = {}) {
try {
const res = await apiFetch("/api/health", { retry: false, timeout: 5000, acceptNotOk: true });
const res = await apiFetch("/api/monitoring/health", {
retry: false,
timeout: 5000,
acceptNotOk: true,
});
if (!res.ok) {
console.error(`HTTP ${res.status}`);
return 1;
}
const health = await res.json();
const components = health.components || health.breakers || {};
const components = health.components || health.circuitBreakers || {};
for (const [name, info] of Object.entries(components)) {
const status =
typeof info === "object" ? info.state || info.status || "unknown" : String(info);

View File

@@ -351,11 +351,35 @@ async function runWithSupervisor(
if (up) {
if (useTray) await maybeStartTray(dashboardPort, apiPort, supervisor);
onReady(dashboardPort, apiPort, noOpen, startedAt);
} else {
reportReadinessTimeout(dashboardPort, supervisor);
}
});
}
}
// #6321: waitForServer resolving `false` used to fall through silently — the CLI
// printed the banner + "⏳ Starting server..." and then produced ZERO further
// output forever, even though the child process may well have crashed or be
// stuck (issue reports show the server sometimes actually comes up later, or is
// reachable directly while the CLI still looks hung). Surface a clear diagnostic
// plus whatever stdout/stderr the child buffered instead of going silent.
export function reportReadinessTimeout(dashboardPort, supervisor) {
console.error(
`\n\x1b[33m⚠ Server did not respond within 60s.\x1b[0m It may still be starting, or may` +
` have failed silently.`
);
console.error(` Try: curl -I http://localhost:${dashboardPort}/api/monitoring/health`);
console.error(` Or: rerun with \x1b[36m--log\x1b[0m to see live server output.\n`);
const recentLog = supervisor?.getRecentLog?.() ?? [];
if (recentLog.length) {
console.error("--- Recent server output ---");
recentLog.forEach((l) => console.error(l));
console.error("--- End recent output ---\n");
}
}
let _killTray = null;
function killTrayIfActive() {
if (_killTray) {

1262
bin/cli/locales/zh-TW.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -37,6 +37,7 @@ function inferSchema(sample) {
function formatCell(v, col) {
if (v == null) return "";
if (col.formatter) return col.formatter(v);
if (typeof v === "object") return JSON.stringify(v);
return String(v);
}

View File

@@ -35,22 +35,32 @@ export class ServerSupervisor {
// heap via NODE_OPTIONS (a CLI arg would shadow/override their value). The
// calibrated heap is already carried by env.NODE_OPTIONS either way.
const heapArgs = buildNodeHeapArgs(process.env, this.memoryLimit);
// #6321: stdout used to be discarded (`"ignore"`) whenever `--log`/OMNIROUTE_SHOW_LOG
// wasn't set (the default) — any debug/pino output written to stdout vanished
// silently, so a boot that never becomes ready looked like a dead hang with zero
// output even at APP_LOG_LEVEL=debug. Pipe stdout too and buffer it alongside
// stderr so a readiness timeout can surface what the child actually printed.
this.child = spawn("node", [...heapArgs, this.serverPath], {
cwd: dirname(this.serverPath),
env: this.env,
stdio: showLog ? "inherit" : ["ignore", "ignore", "pipe"],
stdio: showLog ? "inherit" : ["ignore", "pipe", "pipe"],
});
writePidFile("server", this.child.pid);
const bufferOutput = (data) => {
const lines = data.toString().split("\n").filter(Boolean);
this.crashLog.push(...lines);
if (this.crashLog.length > CRASH_LOG_LINES) {
this.crashLog = this.crashLog.slice(-CRASH_LOG_LINES);
}
};
if (this.child.stdout) {
this.child.stdout.on("data", bufferOutput);
}
if (this.child.stderr) {
this.child.stderr.on("data", (data) => {
const lines = data.toString().split("\n").filter(Boolean);
this.crashLog.push(...lines);
if (this.crashLog.length > CRASH_LOG_LINES) {
this.crashLog = this.crashLog.slice(-CRASH_LOG_LINES);
}
});
this.child.stderr.on("data", bufferOutput);
}
this.child.on("error", (err) => this.handleExit(-1, err));
@@ -107,6 +117,12 @@ export class ServerSupervisor {
}, delay);
}
// #6321: exposes the buffered stdout+stderr lines so a caller (e.g. a readiness
// timeout) can print what the child actually said instead of silence.
getRecentLog() {
return [...this.crashLog];
}
dumpCrashLog() {
console.error("\n--- Server crash log ---");
this.crashLog.forEach((l) => console.error(l));

View File

@@ -65,34 +65,52 @@ export function sleep(ms) {
// cold start due to filesystem watchers, antivirus, etc.) get a working
// "server ready" signal instead of a phantom timeout while the server is
// still booting. TCP fallback marks the server as ready when the port
// has been listening for >= 3s consecutively but /api/monitoring/health
// has not yet been mounted — common during dev cold start.
// has been listening for >= 3s consecutively AND the health route is
// actively rejecting/resetting connections fast (route not mounted yet,
// but the HTTP server is clearly alive and responsive) — never for a
// socket that merely accepts TCP and then hangs without ever completing
// a single request (#6800: that's a still-booting/CPU-bound process, not
// a "route not mounted" gap, and must NOT be reported as ready).
export async function waitForServer(port, timeout = 60000) {
const start = Date.now();
let tcpListeningSince = null;
while (Date.now() - start < timeout) {
try {
const res = await fetch(`http://localhost:${port}/api/monitoring/health`, {
signal: AbortSignal.timeout(2000),
});
if (res.ok) return true;
// Server responded but health endpoint is not ready yet — keep
// polling, but the fact that we got a response means TCP is open.
const outcome = await pollHealthOnce(port);
if (outcome === "ready") return true;
if (outcome === "fast-reject") {
if (tcpListeningSince === null) tcpListeningSince = Date.now();
} catch {
const listening = await isPortListening(port).catch(() => false);
if (listening) {
if (tcpListeningSince === null) tcpListeningSince = Date.now();
if (Date.now() - tcpListeningSince >= 3000) return true;
} else {
tcpListeningSince = null;
}
if (Date.now() - tcpListeningSince >= 3000) return true;
} else {
// "hanging" (request timed out with no response at all) or
// "not-listening" — neither counts toward the grace window.
tcpListeningSince = null;
}
await sleep(500);
}
return false;
}
// Polls /api/monitoring/health once and classifies the outcome:
// - "ready": got a 2xx HTTP response.
// - "fast-reject": got a non-2xx HTTP response, or the connection was
// actively refused/reset (not a timeout) — the HTTP server is alive and
// answering quickly, just not routing this endpoint yet (#2460).
// - "hanging": the request timed out waiting for any response — the
// process accepted the TCP connection but never answered (#6800).
// - "not-listening": nothing is accepting connections on the port at all.
async function pollHealthOnce(port) {
try {
const res = await fetch(`http://localhost:${port}/api/monitoring/health`, {
signal: AbortSignal.timeout(2000),
});
return res.ok ? "ready" : "fast-reject";
} catch (err) {
if (err?.name === "TimeoutError") return "hanging";
const listening = await isPortListening(port).catch(() => false);
return listening ? "fast-reject" : "not-listening";
}
}
async function isPortListening(port) {
const net = await import("node:net");
return new Promise((resolve) => {

42
changelog.d/README.md Normal file
View File

@@ -0,0 +1,42 @@
# changelog.d/ — changelog fragments
**A PR never edits `CHANGELOG.md` directly during the cycle.** Instead it adds ONE new
file here — its changelog entry as a *fragment*. Two PRs never touch the same file, so
changelog merge conflicts (the "CHANGELOG-eat" cascade that forced a re-sync push + full
CI re-run after every sibling merge) are structurally impossible.
## Convention
| Directory | Aggregates under |
| -------------- | ----------------------- |
| `features/` | `### ✨ New Features` |
| `fixes/` | `### 🐛 Bug Fixes` |
| `maintenance/` | `### 📝 Maintenance` |
- **Filename**: `<PR-number>-<short-slug>.md` (e.g. `fixes/6700-dockerfile-better-sqlite3.md`).
The PR number prefix keeps aggregation order deterministic.
- **Content**: the exact bullet line(s) that should land in `CHANGELOG.md`, starting with
`- `. Multi-line (continuation) bullets are fine. Keep the repo's credit format:
`(#PR — thanks @user)`.
- One fragment per PR (rarely more, e.g. a PR that both fixes and adds).
## Example
`changelog.d/fixes/6496-cloudflare-relay-worker-syntax.md`:
```markdown
- **fix(providers):** Cloudflare relay Worker deploys use Service Worker syntax with `body_part` metadata ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)) — thanks @SeaXen
```
## Aggregation
The release captain (or `/generate-release`) folds all fragments into `CHANGELOG.md` and
deletes them:
```bash
node scripts/release/aggregate-changelog.mjs # write + delete fragments
node scripts/release/aggregate-changelog.mjs --dry-run # preview only
```
Fragment well-formedness is enforced by `npm run check:changelog-integrity` (the same
gate that guards against CHANGELOG-eat for legacy direct edits).

View File

View File

@@ -0,0 +1 @@
- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky).

View File

@@ -0,0 +1 @@
- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 5970% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:<reason>` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661).

View File

@@ -0,0 +1 @@
- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari).

View File

@@ -0,0 +1 @@
- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211)

View File

@@ -0,0 +1 @@
- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit)

View File

@@ -0,0 +1 @@
- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev).

View File

@@ -0,0 +1 @@
- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc).

View File

@@ -0,0 +1 @@
- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell).

View File

@@ -0,0 +1 @@
- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn).

View File

@@ -0,0 +1 @@
- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell).

View File

@@ -0,0 +1 @@
- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/<PR>-<slug>.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`.

View File

@@ -0,0 +1 @@
- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan).

View File

@@ -0,0 +1 @@
- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127).

View File

@@ -0,0 +1 @@
- **feat(dashboard):** search box on the Playground's raw model `<select>` (#4086) — the shared `ModelSelectModal` (combo builder + CLI-code cards) already had search, but Playground's `StudioConfigPane` model dropdown stayed a flat unsearchable list, unusable once a provider like OpenRouter contributed 50+ models. Typing now filters the dropdown (Turkish-safe accent/case-insensitive match via `matchesSearch`), while the currently selected model always stays pinned in the list even if it no longer matches the query, so typing never silently swaps the active selection. Reuses the existing `common.search` i18n key (already translated in all 42 locales) — no new translation key needed. Regression guard: `tests/unit/playground-model-selection-3731.test.ts` (`filterModelsByQuery`), `tests/unit/ui/playground-model-search-4086.test.tsx`.

View File

@@ -0,0 +1 @@
- **feat(usage):** Antigravity/agy quota widget now surfaces the **weekly** window alongside the existing per-model 5-hour window ([#4017](https://github.com/diegosouzapw/OmniRoute/issues/4017)) — the weekly limit isn't part of the per-model `retrieveUserQuota` response the fetcher already calls; it only appears in a separate, undocumented `retrieveUserQuotaSummary` RPC that groups models into families ("Gemini Models", "Claude and GPT models") with one weekly bucket per family. A new `usage/antigravityWeeklyQuota.ts` leaf fetches that RPC (cached, best-effort — a failure or unavailable RPC never breaks the existing per-model quotas) and parses the weekly bucket per group into `gemini_weekly`/`claude_gpt_weekly` quota entries, merged into the same `quotas` map the widget already renders generically. Regression guard: `tests/unit/antigravity-weekly-quota-4017.test.ts` (bucket parsing, the alternate `quotaSummary`-nested envelope, and end-to-end merge via `getUsageForProvider`).

View File

@@ -0,0 +1 @@
- **feat(codex):** Codex CLI compatibility shim — the Responses API `response.created`/`response.in_progress`/`response.completed` payloads now carry a `model` field (previously absent), and for Codex-CLI-originated requests it echoes the client-requested, effort-suffixed model id (e.g. `gpt-5.5-xhigh`) instead of the bare upstream id (`gpt-5.5`), so the Codex CLI status line/model button shows the active reasoning effort ([#3697](https://github.com/diegosouzapw/OmniRoute/issues/3697)). `openaiToOpenAIResponsesResponse` (`open-sse/translator/response/openai-responses.ts`) now threads the upstream model into the Responses event objects; a new `isCodexOriginatedHeaders()` (`open-sse/config/codexIdentity.ts`, reusing PR #3481's `originator`/User-Agent detection) makes chatCore's existing opt-in `echoRequestedModelName` (#1311) model-echo pipeline fire automatically for Codex clients regardless of the setting, detected by request headers so it still applies when `codex/gpt-5.5-xhigh` is routed through a combo to a non-codex upstream; `echoModelInObject`/`echoModelInSseLine` (`open-sse/services/responseModelEcho.ts`) now also rewrite the nested `response.model` field the Responses API uses. `/v1/models` still returns `models: []` for Codex (unchanged). Regression guard: `tests/unit/codex-effort-model-echo-3697.test.ts`.

View File

@@ -0,0 +1 @@
- **Z.ai Web (free web-session provider)**: new `zai-web` web-cookie provider drives the free chat.z.ai consumer chat UI via a pasted browser session cookie, distinct from the existing API-key `zai`/`glm`/`glm-cn`/`glmt` providers (`api.z.ai`) — modeled on the `doubao-web`/`venice-web` cookie executors and the pre-existing `chatglm-web` credential requirement/token-extraction entries. `ZaiWebExecutor` (`open-sse/executors/zai-web.ts`) posts to `chat.z.ai/api/chat/completions` with the cookie forwarded both as `Cookie` and as `Authorization: Bearer <token>`, and normalizes both z.ai's internal `delta_content`/`phase` SSE envelope and a pass-through OpenAI-shaped `choices[].delta` frame into standard chat-completion chunks. Registered in `WEB_COOKIE_PROVIDERS`, `WEB_SESSION_CREDENTIAL_REQUIREMENTS`, the provider registry (`zai-web` entry, GLM-4.6/4.5/4.5V models), and `tokenExtractionConfig.ts` for in-app cookie capture. Regression guard: `tests/unit/executor-zai-web.test.ts` (16 tests — token extraction, frame parsing for both SSE shapes, streaming and non-streaming aggregation, error paths). (#4056)

View File

@@ -0,0 +1 @@
- **feat(compression):** update the vendored **GCF** codec behind the **Headroom** engine to spec **v3.2 (nested flattening)** ([#6837](https://github.com/diegosouzapw/OmniRoute/issues/6837)). Homogeneous arrays whose rows carry nested objects/arrays now tabularize via `>`-prefixed path fields instead of a low-yield per-row fallback, so nested MCP tool-result rows (`meta:{...}`, `tags:[...]`) compact like flat rows. On representative shapes the update takes deeply-nested payloads the old codec left near-uncompressed from ~3% to ~32% vs JSON (k8s pods, `cl100k_base`), with shallow-nested rows seeing a small bump and flat arrays unchanged. Re-vendored from current gcf-typescript (zero runtime deps, MIT, SPDX-marked, generic-profile only); also folds in the `[N]:` inline-array quoting fix and canonical decimal formatting. Round-trip stays lossless (order-insensitive), and the decoder is hardened against prototype pollution (a `__proto__`/`constructor` path segment never mutates `Object.prototype`, and keys shadowing built-ins like `toString` now round-trip correctly instead of misparsing). Regression guard: `tests/unit/compression/headroom-smartcrusher.test.ts` (deep-nested + prototype-pollution cases).

View File

@@ -0,0 +1,2 @@
- **feat(providers):** Add GPT-5.6 support across OpenAI API, Codex, and ChatGPT Web, including Codex Max/Ultra efforts, VS Code metadata, Fast-tier credit accounting, curated live discovery, the Codex 0.144.1 client identity, and correct chat routing for models that also support image generation ([#6862](https://github.com/diegosouzapw/OmniRoute/pull/6862)) - thanks @backryun
- **chore(providers):** Align emitted Claude Code identity headers, bridge fingerprints, provider profiles, and documented defaults with claude-cli 2.1.207 ([#6862](https://github.com/diegosouzapw/OmniRoute/pull/6862)) - thanks @backryun

View File

View File

@@ -0,0 +1 @@
- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142)

View File

@@ -0,0 +1 @@
- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272)

View File

@@ -0,0 +1 @@
- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276)

View File

@@ -0,0 +1 @@
- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun

View File

@@ -0,0 +1 @@
- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2).

View File

@@ -0,0 +1 @@
- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev).

View File

@@ -0,0 +1 @@
- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev).

View File

@@ -0,0 +1 @@
- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343)

View File

@@ -0,0 +1 @@
- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377)

View File

@@ -0,0 +1 @@
- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799)

View File

@@ -0,0 +1 @@
- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409)

View File

@@ -0,0 +1 @@
- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491)

View File

@@ -0,0 +1 @@
- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524)

View File

@@ -0,0 +1 @@
- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky).

View File

@@ -0,0 +1 @@
- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)).

View File

@@ -0,0 +1 @@
- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev).

View File

@@ -0,0 +1 @@
- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623)

View File

@@ -0,0 +1,2 @@
- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628)
- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802)

View File

@@ -0,0 +1 @@
- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari).

View File

@@ -0,0 +1 @@
- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634)

View File

@@ -0,0 +1 @@
- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637)

View File

@@ -0,0 +1 @@
- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub).

View File

@@ -0,0 +1 @@
- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun).

View File

@@ -0,0 +1 @@
- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698).

View File

@@ -0,0 +1 @@
- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis)

View File

@@ -0,0 +1 @@
- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin)

View File

@@ -0,0 +1 @@
- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit)

View File

@@ -0,0 +1 @@
- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820)

View File

@@ -0,0 +1 @@
- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk)

View File

@@ -0,0 +1 @@
- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev

View File

@@ -0,0 +1 @@
- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`.

View File

@@ -0,0 +1 @@
- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584)

View File

@@ -0,0 +1 @@
- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills/<id>/SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`).

View File

@@ -0,0 +1 @@
- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819).

View File

@@ -0,0 +1 @@
- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`.

View File

@@ -0,0 +1 @@
- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json``pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage).

View File

@@ -0,0 +1 @@
- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact).

View File

@@ -0,0 +1 @@
- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`.

View File

@@ -0,0 +1 @@
- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab)

View File

@@ -0,0 +1 @@
- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant)

View File

@@ -0,0 +1 @@
- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text.

View File

@@ -0,0 +1 @@
- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`.

View File

@@ -0,0 +1 @@
- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path).

View File

@@ -0,0 +1 @@
- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`.

View File

@@ -0,0 +1 @@
- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a *local cache* already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account).

View File

@@ -0,0 +1 @@
- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta``reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters).

View File

@@ -0,0 +1 @@
- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756).

View File

@@ -0,0 +1 @@
- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos).

Some files were not shown because too many files have changed in this diff Show More