1961 Commits

Author SHA1 Message Date
Diego Rodrigues de Sa e Souza
872895c172 Merge pull request #3092 from diegosouzapw/release/v3.8.9
Release v3.8.9
2026-06-04 00:30:02 -03:00
diegosouzapw
b6bda19919 fix(quality): explicit promise-existence checks + protocol-aware WebDAV URL
SonarCloud new-code findings:
- combo.ts / sync-models route: `if (cachedPromise)` -> `!= null` (intentional
  in-flight-promise reuse; explicit existence check, no behaviour change).
- ObsidianSourceCard WebDAV URL: inherit window.location.protocol instead of
  hard-coding http:// (https when behind a TLS proxy) — clears the http hotspot.
2026-06-03 23:53:03 -03:00
NOXX - Commiter
223374221f feat(kiro): add Claude Opus 4.8 to the Kiro model catalog (#3131)
Kiro (AWS CodeWhisperer) tops out at Claude Opus 4.7 in the registry, but the latest Opus 4.8 is already served by the Claude Code provider and Kiro's executor passes the model id through to CodeWhisperer verbatim. Expose claude-opus-4.8 on the kiro provider so clients can select it via kiro/claude-opus-4.8.

- providerRegistry: add claude-opus-4.8 (1M context, 128k output) above 4.7

- pricing: add claude-opus-4.8 (and the previously-missing 4.7) to the kiro pricing block at Kiro's standard Opus rate so usage cost is non-zero

- tests: assert kiro exposes claude-opus-4.8 with matching context/output + pricing
2026-06-03 23:51:21 -03:00
diegosouzapw
8dd9749a93 fix(build): correct modelResolution import path in responses route (#3113)
The webpack build failed: route.ts imported '../internal/codex-responses-ws/
modelResolution' (resolves to api/v1/internal/, which doesn't exist). The module
lives at api/internal/codex-responses-ws/. Switched to the @/app/api/... alias.
typecheck/tests passed (tsx resolves leniently; tests import the module directly),
only the production build caught it.
2026-06-03 19:47:42 -03:00
diegosouzapw
cad93f35ce fix(lint): escape quotes in ObsidianSourceCard JSX (react/no-unescaped-entities) 2026-06-03 18:58:29 -03:00
Oğuzhan Sert
8dc93ade6d fix(i18n): Turkish locale-aware search and sorting (#3115)
* feat(i18n): add Turkish locale-aware text helpers (search/sort)

* test(i18n): cover null/whitespace edges + document compareTr/normalize contract

* fix(i18n): route dashboard search through Turkish-safe matchesSearch

* fix(i18n): sort user-visible lists with Turkish collation (compareTr)

* fix(i18n): keep providerId tiebreaker as ASCII sort (technical id)

* docs(i18n): document intentional lang=en in global-error boundary

* chore(lint): guard against locale-unsafe toLowerCase().includes search

* fix(i18n): migrate missed provider-name search + harden lint disable placement

* fix(i18n): downgrade no-restricted-syntax to warn (incremental adoption)

The rule errored on ~19 pre-existing toLowerCase().includes() call-sites in
src/app accumulated since this PR's base. Keep it as a warning so the guard-rail
guides future code without breaking the 0-errors lint gate (project policy:
0 errors, warnings tolerated).

---------

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-06-03 18:58:01 -03:00
Tubagus
a718558d68 perf(logs): fix browser freeze and network saturation on /dashboard/logs (#3109)
* feat(providers): implement bulk paste for extra API keys

Adds `parseExtraApiKeys` utility to process multi-line key inputs.
Integrates bulk paste functionality into the provider connection modal.
Users can now paste multiple API keys, one per line, into the input field.
Provides notifications for successfully added keys and ignored duplicates.
Adds a "Delete all" button to clear all extra API keys.
Updates i18n messages for new features and improved key masking/pluralization.

* refactor(providers): streamline API key bulk paste and i18n

Remove unused return from `handleAddParsedExtraKeys` to clean up code.
Adjust `onPaste` to allow default paste for single-line input, improving UX.
Remove obsolete bulk paste UI translation keys to reduce bundle size.
Update pluralization for bulk paste messages to ensure correct grammar.
Refine Portuguese (Brazil) API key translations for clarity.

* fix(logs): apply code review feedback - robust signature, immediate fetch on tab restore, reuse memoized apiKeyCount

* test(logs): extract pure polling/signature helpers + cover them (#3109)

Hard rule #8: the perf fix touched src/ without tests. Extract computeLogsSignature,
shouldAutoRefresh and resolveInitialVisibility into a pure module and unit-test
them (change-detection, first-page polling guard, SSR/hidden-tab visibility init).
Also fixes visibleRef to honor the real visibilityState on mount instead of
hardcoding true (no poll when mounted in a hidden tab).

---------

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-06-03 18:35:09 -03:00
EmpRider
51345bf2e9 fix(cli): handle Windows exe healthchecks with spaces (#3111)
* fix(cli): handle Windows exe healthchecks with spaces

* Update src/shared/services/cliRuntime.ts

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* Update tests/unit/cli-runtime-extended.test.ts

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* fix(cli): pass command to spawn unquoted; export shouldUseShellForCommand

Remove the manual "${command}" interpolation into the shell command (hard rule
#13 violation, and redundant — Node quotes for cmd.exe when shell:true; .exe runs
with shell:false where the OS handles spaces via argv). Export the helper and add
a cross-platform test asserting non-Windows never uses the shell.

---------

Co-authored-by: Empire Rider <anuruddhawijesiri@gmail.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-06-03 18:30:03 -03:00
Ahmet Çetinkaya
84b5caeeb9 fix(sse): bound Antigravity 429 retry loop and lock quota-exhausted accounts for full reset window (#3122)
* fix(sse): bound Antigravity short-retry 429 loop per endpoint

A persistent 429 on the short-retry branch (retryAfterMs ≤ 60s) looped
forever on the same endpoint because the branch did `urlIndex--; continue`
without checking the shared retry counter. Production log showed 77
consecutive 429s on one daily endpoint/account with zero fallback.

Gate the short-retry branch on `retryAttemptsByUrl[urlIndex] < MAX_AUTO_RETRIES`
(mirroring the already-bounded sibling), so a persistent 429 retries at most
3× per endpoint across all 3 base URLs then returns the 429 to the account-
fallback layer.

Regression test: 'bounds a persistent short-retry 429' in
tests/unit/executor-antigravity.test.ts — asserts 12 total attempts
(3 endpoints × 4) and a returned 429 with zero hang.

* fix(sse): lock Antigravity quota-exhausted account for full reset window

After the retry-loop bound, OmniRoute fell over to the next account but
re-selected the exhausted one first on every subsequent request (~60s wasted
per request). Root cause: the 429 body 'Individual quota reached. Contact
your administrator to enable overages. Resets in 164h27m24s.' was not
recognized as quota exhaustion, so the model was locked for only ~5s instead
of the real 6.8-day reset window.

Two detector fixes (mirrors Antigravity-Manager rate_limit.rs set_lockout_until):

1. classify429.ts — add QUOTA_PATTERNS: /individual quota reached/i,
   /quota reached/i, /enable overages/i so looksLikeQuotaExhausted() fires.
2. accountFallback.ts — same patterns in classifyErrorText(); extend
   parseRetryFromErrorText() to parse 'Resets? in XhYmZs' (reusing the
   existing computeDurationMs helper) so the exact reset duration reaches
   recordModelLockoutFailure as exactCooldownMs (uncapped, per user choice).

The lockout machinery already stores until = now + cooldownMs with no clamp,
bypasses getScaledCooldown when exactCooldownMs > 0, and keeps the longer of
existing/new, so the full 164h window flows through intact.

New patterns stay specific — plain 'too many requests'/'rate limit exceeded'
messages still classify as rate_limit.

Verified end-to-end against the real message:
- classify429 → quota_exhausted
- parseRetryFromErrorText → 592044000 ms (164h27m24s exactly)
- checkFallbackError → usedUpstreamRetryHint: true, cooldownMs: 592044000

* refactor(account-fallback): simplify error parsing and add cooldown safety

- Implement a 30-day cap on parsed retry durations to prevent indefinite account lockouts.
- Replace manual string matching with `looksLikeQuotaExhausted` for more robust quota detection.
- Streamline regex logic in `parseRetryFromErrorText` for better readability.
- Add unit tests for extreme cooldown values and free-tier exhaustion scenarios.

* fix(429): drop over-broad /quota reached/ pattern, keep specific matches

The bare /quota reached/ would also flag transient per-minute limits like
'request quota reached, retry in 60s' as quota_exhausted (multi-hour lock).
The Antigravity message is still caught by /individual quota reached/. Added a
regression assertion proving the transient case stays a rate_limit.

---------

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-06-03 18:29:59 -03:00
Diego Rodrigues de Sa e Souza
ed170229e7 fix(responses): resolve bare ChatGPT model ids to codex on HTTP fallback path (#3113)
When the Codex CLI falls back from WebSocket to HTTP (after 1008 Policy
Violation or reconnect exhaustion), it POSTs to /v1/responses with the
bare model id it was configured with (e.g. "gpt-5.5") — never the
provider-prefixed form. OmniRoute's normal routing resolved that bare id
to openrouter, not codex, producing:

  "No credentials for provider: openrouter"

Fix: add resolveResponsesApiModel() (extending modelResolution.ts) and
call it in /v1/responses before delegating to handleChat. The function
applies the same codex-preference logic as resolveCodexWsModelInfo:

  bare "gpt-5.5" → codex/gpt-5.5 has a codex provider → rewrite 
  bare "gpt-4o"  → codex/gpt-4o not in registry      → pass through 
  "anthropic/x"  → has "/" → skip resolution           → pass through 

Errors are caught; original request is returned on any failure.
8 unit tests added (TDD — watched each fail before implementing).

Refs: openai/codex#15492, openai/codex#13041, openai/codex#13039
2026-06-03 18:12:34 -03:00
Diego Rodrigues de Sa e Souza
f0f776c310 fix(i18n): fill missing zh-CN and ru UI translations (#3026, #3067) (#3103)
zh-CN and ru were each missing 9 whole sections (~823 keys: quotaPlans, activity, agentBridge, trafficInspector, cliCommon, cliCode, cliAgents, acpAgents, agentSkills) added after the last sweep, so those UI strings fell back to English. Filled via the canonical i18n sync+translate pipeline (scripts/i18n/sync-ui-keys.mjs --translate-markers). Both catalogs are now at full key parity with en.json (70 sections / 8025 keys), 0 __MISSING__ markers, valid JSON, Prettier-clean.
2026-06-03 08:04:17 -03:00
Diego Rodrigues de Sa e Souza
5f7f74dc6a fix(dashboard): qualify vendor-namespaced Playground models with provider prefix (#3050) (#3102)
The provider Playground (LlmChatCard) only added the providerId/ prefix to models without a slash, so vendor-namespaced ids (moonshotai/kimi-k2.6, nvidia/zyphra/...) were sent bare and rejected with 'Ambiguous model' when the same id exists under multiple providers. Extracted qualifyPlaygroundModel() which always prefixes with the provider unless already qualified. Bug 1 ('unhashable type: dict') is an upstream NVIDIA NIM server error, not OmniRoute. Tests: 4 cases for the qualifier.
2026-06-03 07:56:16 -03:00
Diego Rodrigues de Sa e Souza
5f3b1e8cde fix(db): dedup duplicate API keys per provider on connection create (#3023) (#3100)
createProviderConnection deduped apikey connections only by (provider, name). Adding the same key under a different/blank name created a duplicate row. It now also matches by the decrypted key value (AES-GCM ciphertext is non-deterministic, so we decrypt+compare plaintext, trimmed) and updates the existing connection instead. Tests cover same-key dedup, whitespace-variant dedup, and distinct-key separation.
2026-06-03 07:52:10 -03:00
Diego Rodrigues de Sa e Souza
8a25d9e229 fix(dashboard): make 'Import from /models' work for no-auth providers (#3047) (#3099)
No-auth providers (OpenCode Free) have no connection row, so handleImportModels returned early and /api/providers/[id]/models 404'd — the import button silently no-op'd. The route now serves the provider's registry/static model catalog when called with a no-auth provider id, and handleImportModels falls back to the provider id when there is no connection. Test: route returns the opencode catalog (source local_catalog) and still 404s for unknown ids.
2026-06-03 07:48:19 -03:00
Diego Rodrigues de Sa e Souza
365c29a115 fix(providers): forward Grok sso-rw cookie to fix anti-bot 403 (#3063) (#3098)
grok-web only sent the 'sso' cookie; Grok's anti-bot now rejects (403 code 7) requests missing the paired 'sso-rw' write cookie. Adds buildGrokCookieHeader() which emits sso plus sso-rw when the pasted blob carries it (never a phantom sso-rw from a bare value), used by both the executor and the connection validator. Updates the grok-web authHint. Tests: 6 new buildGrokCookieHeader cases; grok-web 62/62 unchanged.
2026-06-03 07:39:02 -03:00
Diego Rodrigues de Sa e Souza
7042d562c4 fix(sse): scope ollama-cloud per-model 403 to model lockout, not connection cooldown (#3027) (#3096)
A per-model subscription/permission 403 from a passthrough provider (hasPerModelQuota) now locks only the failing model instead of cooling the whole connection, so free models on the same key keep serving and repeated paid-model 403s don't escalate a connection-wide backoff. Generalizes the grok-web 403 precedent; terminal/credential 403s still deactivate the connection (guarded by resolveTerminalConnectionStatus). TDD: 3 tests in sse-auth.test.ts (2 reproductions fail pre-fix, regression guard passes both ways).
2026-06-03 07:25:50 -03:00
Xiangzhe
948f232517 fix: sync SiliconFlow models from configured endpoint (#3094)
Integrated into release/v3.8.9. SiliconFlow tests pass.
2026-06-03 07:19:38 -03:00
diegosouzapw
51d2ca8151 feat(db): add apiKeyContextSources module + migration 092
Implements per-API-key context source configuration table and CRUD
functions required by the Obsidian PR. Restores getApiKeyContextSource
in obsidian.ts (was stubbed to null during conflict resolution).
11/11 tests pass in obsidian-config.test.ts.
2026-06-03 07:04:52 -03:00
Brandon Bennett
9db306e2f8 feat(observability): add Obsidian context source with 24 MCP tools (#3077)
Integrated into release/v3.8.9. Fixes applied: removed .serena/ from repo (gitignored), resolved import conflicts preserving agentSkillTools, fixed TS errors.
2026-06-03 06:45:04 -03:00
Paijo
9141e98458 fix(providers): fix claude-web 403, move no-auth providers out of web-cookie (#3090)
Integrated into release/v3.8.9 — resolved conflicts with release branch (allowAutoSolve:true preserved, duckduckgo-web correctly kept in NOAUTH_PROVIDERS).
2026-06-03 06:35:45 -03:00
diegosouzapw
5943e5d528 feat: add Codex WS flag and Xiaomi MiMo usage tracking
Add a global OMNIROUTE_CODEX_WS_ENABLED kill-switch for Codex WebSocket transport, defaulting to enabled if feature flag lookup fails.

Track Xiaomi MiMo monthly token usage from OmniRoute usage history and expose it through the usage fetcher provider list.
2026-06-03 00:41:11 -03:00
diegosouzapw
0f900f1d56 fix(usage): handle MiniMax coding plan percent quotas
Support MiniMax Coding Plan quota responses that expose remaining usage as
percentages instead of request counts, including the text quota surfaced under
the `general` model.

Also document Codex CLI WebSocket configuration, clarifying that bare ChatGPT
model ids must be used instead of `codex/`-prefixed ids.
2026-06-03 00:41:10 -03:00
diegosouzapw
705ab8e690 fix(quality): clear SonarCloud quality-gate findings on PR #2930
Quality Gate was failing on New Code with 1 hotspot, 1 vulnerability and
4 reliability bugs. Resolved each at the source (no SonarCloud mark-as-safe):

Reliability (4 bugs → rating back to A):
- usage.ts: drop duplicate `case "opencode-go"` (S1862) — the 2nd case was
  dead (first match wins) and would have called the wrong usage fetcher.
  Also de-duplicate the same id in USAGE_FETCHER_PROVIDERS (mirrors the switch;
  prevented a double quota-fetcher registration).
- ApiManagerPageClient.tsx: a dangling `.find(...)?.timestamp || null` expression
  (S905) discarded the better matcher — `lastUsed` silently lost the
  name-fallback for legacy logs without apiKeyId. Assign the complete matcher.
- chat.ts / auth.ts: `Promise` used in a boolean conditional (S6544). Both are
  intentional (memoized promise / mutex default), not a forgotten await —
  made the intent explicit via `!== null` and `??` (behaviour identical).

Security (vulnerability → rating back to A):
- nvidia-startswith-diag.ts: neutralize CR/LF before logging env-derived
  values (S5145 log injection) in the ad-hoc diagnostic script.

Security Hotspot (reviewed → 0 open):
- pluginWorker.ts uses `vm.runInContext` to run plugin code — that IS the
  worker's purpose, inside a hardened vm sandbox (createContext, require
  allow-list of just `crypto`, 10s timeout); not eval/new Function. Suppress
  S1523 scoped only to pluginWorker.ts in sonar-project.properties, with the
  same documented-justification pattern as the existing hotspot suppressions.
2026-06-03 00:17:56 -03:00
diegosouzapw
5b62a4be88 fix(combo): use resolved provider for custom provider_nodes prefixes (#3058 follow-up)
In checkModelAvailable and handleSingleModelChat, when the combo target's
providerId is merely the prefix already encoded in the model string (e.g. "p2"
from "p2/test-model"), prefer the fully-resolved provider (e.g. the generated
custom node id openai-compatible-chat-e2e-p2) so the executor resolves the
custom baseUrl from the connection instead of falling back to the base provider
(real openai). Intentional providerId overrides (providerId not encoded in the
model string) are preserved.

Also fixes the resilience-http-e2e combo tests (cooldown window + DB-write
visibility for the cooled-down-primary skip).
2026-06-02 20:29:41 -03:00
diegosouzapw
326a219620 fix(memory): recent strategy must not relevance-filter by prompt
The "recent" memory strategy maps to the internal "exact" retrieval path, whose
post-query relevance filter (score > 0) silently dropped recent memories whose
text didn't overlap the current prompt. Since the user-facing strategy enum is
only recent|semantic|hybrid (no "exact"), forwarding the prompt as `query` for
"recent" always engaged that filter, so recency-based injection returned nothing
when the prompt was unrelated to the stored memory.

Skip query forwarding for the "recent" strategy so retrieveMemories returns the
most recent memories (ORDER BY created_at DESC) regardless of prompt overlap.
Semantic/hybrid still forward the query for vector search.

Fixes the chat-pipeline + memory-pipeline integration memory-injection tests.
2026-06-02 18:02:28 -03:00
diegosouzapw
72b4804c1b Merge main into release/v3.8.8
Back-merge to resolve PR #2930 (release/v3.8.8 -> main) conflicts. Release is a
superset of main's features, so all ~44 content conflicts resolved to the
release ("ours") version; generated .source/* dropped.

Reconciliation:
- auth.ts: port #3058 (getProviderSearchPool expands custom provider_nodes
  prefixes to internal connection ids) — release lacked this main fix.
- quota-plan-registry.test.ts: align knownProviders() 6 -> 10 (pre-existing
  stale assertion vs the registry).
2026-06-02 17:46:48 -03:00
diegosouzapw
f5acb60cac chore(mitm): fix stub comment accuracy + broaden stub-drift guard (PR review)
Addresses findings from the multi-agent PR review of the #3066 fix:

- manager.stub.ts comments: the previous inline comment claimed the throwing ops
  (getMitmStatus/startMitm/stopMitm) are "dynamic-import paths that should never hit
  the stub at runtime" — factually wrong: those are static imports too, baked into the
  bundled build just like getAllAgentsStatus. Rewrote the file header to describe the
  real split — exports with a safe degraded value return it (getCachedPassword/
  setCachedPassword/clearCachedPassword → null/no-op, getAllAgentsStatus → []) while
  getMitmStatus/startMitm/stopMitm throw STUB_ERROR — and trimmed the inline comment.
  Comment-only; no runtime/build change (the export still exists).

- stub-drift guard test: now scans ALL of src/ instead of only src/app —
  src/lib/tailscaleTunnel.ts statically imports getCachedPassword/setCachedPassword
  from @/mitm/manager and is pulled into routes transitively, so the src/app-only scan
  had a false-negative blind spot. Also skips inline `type` imports (erased at build,
  need no runtime export) and detects stub exports from declaration AND `export { … }`
  forms (no false-positive if the stub later uses class/re-export).

Verified: next-config suite 4/4, typecheck:core / lint clean.
2026-06-02 13:44:25 -03:00
diegosouzapw
7c2dc1cde6 refactor(build): graceful agent-status stub + robust stub-drift guard (review feedback)
Follow-up to 146244b8f (#3066), addressing optional review suggestions:

- manager.stub.ts: getAllAgentsStatus now returns [] (the truthful "no agents"
  state, type-faithful) instead of throwing. Unlike the dynamic-import heavy ops,
  this is a STATIC import baked into the Turbopack/bundled build, so it is
  legitimately reached at runtime there — returning an empty list degrades
  gracefully instead of erroring. (Functionally inert for the existing
  agent-bridge/state route, where getMitmStatus already rejects first.)

- next-config.test.ts: the stub-drift guard no longer hard-asserts a specific
  symbol (getAllAgentsStatus); the generic ">=1 import found" sanity plus the
  missing-exports check remain, so the guard survives an agent-bridge /
  traffic-inspector route being renamed or removed.

typecheck:core / lint / next-config suite (4/4) clean. The export still exists,
so the Turbopack build resolution is unchanged.
2026-06-02 13:33:42 -03:00
diegosouzapw
146244b8f5 fix(build): Turbopack/Docker build — externalize sqlite-vec .so + sync mitm manager stub
The Docker image build (`docker compose --profile cli build`) runs `next build`
with OMNIROUTE_USE_TURBOPACK=1 and failed with two Turbopack errors that the
webpack-based VM build never hits — which is why the VM deploy validated but the
Docker build errored (#3066). The reporter's log was truncated before the real
errors; reproducing `OMNIROUTE_USE_TURBOPACK=1 npm run build` locally surfaced them:

1. node_modules/sqlite-vec-linux-x64/vec0.so — "Unknown module type". sqlite-vec
   ships a native vec0.so loaded at runtime via createRequire(); Turbopack tried to
   bundle the .so. Fixed by adding "sqlite-vec" to serverExternalPackages, exactly
   like better-sqlite3.

2. /api/tools/agent-bridge/state statically imports getAllAgentsStatus from
   @/mitm/manager, which next.config aliases to manager.stub.ts for the Turbopack
   build. The stub did not export getAllAgentsStatus → "Export getAllAgentsStatus
   doesn't exist in target module". Added the export (throws like the other heavy
   ops — MITM/agent-bridge is non-functional in the bundled build anyway).

Tests (tests/unit/next-config.test.ts):
- assert sqlite-vec is in serverExternalPackages.
- new guard: manager.stub.ts must export every name statically imported from
  @/mitm/manager across src/app (catches stub/manager drift — would have caught this).

Verified: OMNIROUTE_USE_TURBOPACK=1 npm run build → EXIT 0 (was: Build error
occurred); webpack build → EXIT 0; typecheck:core / check:cycles / lint clean.

Fixes #3066
2026-06-02 12:22:31 -03:00
diegosouzapw
e438139b03 feat(quota): live real-time codex quota on the page (cascade-safe serialized refresh)
Symptom: freshly-added Codex accounts (e.g. davi/gabriel) showed "No quota data"
even when healthy. Root cause: the quota path reuses the access_token without
refreshing rotating providers (#3019, anti Auth0 family-revocation cascade), so a
Codex account whose short-lived access_token has expired can never surface quota
from the sync — the live fetch returns "Codex token expired".

Fix (opt-in, cascade-safe):
- refreshAndUpdateCredentials gains `allowRotatingRefresh` + a pure exported gate
  `shouldAttemptRotatingRefresh`. The actual token mint is wrapped in
  `serializeRefresh` (one refresh at a time per Auth0 rotation group) — so even N
  concurrent per-account requests can never refresh siblings in parallel.
- The BULK scheduler (syncAllProviderLimits, concurrent) keeps the flag OFF →
  #3019 fully preserved (guardian test codex-quota-sync-no-proactive-refresh stays
  green). Only the on-demand, per-connection path (`GET /api/usage/[connectionId]`)
  opts in.
- Frontend: the quota page auto-fetches LIVE on open for the VISIBLE connections
  that have no cached quota (scoped to what's on screen — not all connections —
  and skips entries already cached), so expired-token Codex accounts surface real
  quota automatically and cascade-safely.

Adds unit coverage for the gate (bulk skips rotating, on-demand allows; non-rotating
always eligible). typecheck / lint clean.
2026-06-02 09:53:50 -03:00
diegosouzapw
c4a993184e fix(quota): never flag rotating-refresh providers expired from the quota sync
The quota-sync path deliberately reuses a rotating-refresh provider's (Codex/
OpenAI/Claude — see refreshSerializer ROTATION_LOCK_GROUP) access_token WITHOUT
proactively refreshing it (#3019, to avoid the Auth0 family-revocation cascade).
When that token is expired the codex usage fetch returns "token expired", and
syncExpiredStatusIfNeeded then flagged the connection testStatus="expired" — a
false-negative: the credential is still valid (expires_at in the future) and the
reactive serialized 401 path refreshes the access_token on next use.

Symptom: freshly-added Codex accounts showed "expired" with no quota on the
quota page, while a providers-page refresh turned them green. They never lost
access — only the quota sync mislabeled them.

Fix: extract the decision into the pure, exported `quotaPathShouldMarkExpired()`
and skip rotating providers (rotationGroupFor !== null). Their status is owned by
the reactive path / connection test, never the quota sync. Adds unit coverage.
2026-06-02 08:47:51 -03:00
diegosouzapw
ccaa0b5f79 fix(quota): block qtSd models for keys with no quota-pool allocation (Check 2.9)
E2E testing on the VPS showed a normal key (empty allowedQuotas) could call a
qtSd/<group>/<provider>/<model> virtual model and route through a shared quota
pool — because the quota-exclusive enforcement (Check 3) only ran when
allowedQuotas was non-empty, so an unallocated key fell through to the normal
model checks and qtSd was served. This is the "empty allowedQuotas = all pools"
gap from the redesign.

Add Check 2.9 in enforceApiKeyPolicy: if the requested model is a qtSd model and
the key is NOT allocated to any quota pool (allowedQuotas empty), reject 403
QUOTA_NOT_ALLOCATED. Allocated keys are unchanged (Check 3 still validates scope).
This matches the owner's rule: only a key selected in a pool may use its qtSd
models. Normal (non-qtSd) model access for normal keys is unchanged.

Test: tests/unit/apikeypolicy-quota-only.test.ts — new case asserts a non-quota
key is blocked from qtSd (QUOTA_NOT_ALLOCATED) yet still uses normal models.
2026-06-02 07:40:08 -03:00
diegosouzapw
8277b98003 feat(api-manager): split keys into Normal vs Quota sections (compact 2-table layout)
The key list stacked many badges in one column (tall/cluttered) and didn't
distinguish quota keys. Now renders two sections — "Normal keys" and "Quota keys"
(purple QUOTA pill) — sharing the same compact table header via an extracted
renderKeyRow(). Quota rows prepend a qtSd-only mode chip + group-name chips
(resolved by fetching /api/quota/pools + /api/quota/groups → poolId→group map).
Empty sections are hidden. i18n en/pt-BR for the new labels.

Source-scan test + i18n parity in api-manager-quota-keys-section.test.ts.
2026-06-02 07:06:00 -03:00
diegosouzapw
8086d2878b fix(ws): codex Responses-over-WebSocket upgrade — clean handshake + bridge-secret auth
Two bugs made `wscat ws://host/v1/responses` fail with
"Transfer-Encoding can't be present with Content-Length":

1. authz/management policy 401'd the proxy's own internal authenticate/prepare
   loopback call to /api/internal/codex-responses-ws (MANAGEMENT-classified, the
   per-process bridge secret wasn't recognized one layer up). Added a tightly-scoped
   carve-out: isValidWsBridgeRequest() honors a timing-safe sha256 match of
   OMNIROUTE_WS_BRIDGE_SECRET (x-omniroute-ws-bridge-secret header) for that exact
   internal path; the route still re-validates the secret. → auth now succeeds → 101.

2. On auth failure the proxy spread the internal fetch's response headers onto the
   raw upgrade socket — a chunked Transfer-Encoding + Next CSP/route-class headers
   collided with writeHttpError's Content-Length framing (and duplicated Content-Type
   via a case-mismatched spread). writeHttpError now strips framing + pipeline/security
   headers (case-insensitive), and the auth-fail callsite no longer forwards them.

Regression test: tests/unit/responses-ws-proxy-headers.test.mjs (exports writeHttpError;
asserts no TE+CL, single Content-Type, no CSP/route-class leak, safe headers forwarded).
2026-06-02 06:02:49 -03:00
diegosouzapw
512e980a9e feat(quota): xiaomi monthly cap + deepseek USD preset; researched plan tiers
- xiaomi-mimo: token plan is MONTHLY (per platform.xiaomimimo.com/token-plan), so
  the seed is now tokens/monthly/4.1B (was weekly).
- deepseek: prepaid in USD — its balance API is already wired (deepseekQuotaFetcher)
  and the fair-share engine supports the usd unit (COUNTABLE_UNITS). Seeded a
  usd/monthly preset so the limit is set by dollar value.
- minimax: documented the real M3 tiers (Plus ~1.633B/Max ~5.053B/Ultra ~9.796B)
  in-comment; EPSILON keeps it manual until tier-aware presets land.
- planRegistry already seeds codex/claude/glm/minimax/kimi/kimi-coding/xiaomi-mimo/
  deepseek/bailian/alibaba; PoolWizard 'Limite' step stays editable.

Researched plan structures + the tier-aware-preset follow-up are in the redesign plan.
2026-06-02 05:17:20 -03:00
Xiangzhe
62f08de540 fix(home): pass providerId to quota widget icons (#3064)
Co-authored-by: xz-dev <xz-dev@users.noreply.github.com>
2026-06-02 05:14:23 -03:00
diegosouzapw
f8c1213722 feat(quota): seed claude plan preset (percent 5h+weekly) in planRegistry
Claude Code (Pro/Max) is a percentage-of-plan quota (5h rolling + weekly cap,
shared Claude+Code); exact token caps are unpublished/task-variable so percent is
the practical unit. Unblocks the PoolWizard 'Limite' pre-fill for claude pools.
Researched plan structures (codex/claude/glm/kimi/minimax/xiaomi) captured in the
quota-share redesign plan.
2026-06-02 05:06:34 -03:00
diegosouzapw
831f82858f feat(quota-share): beta banner, Responses + codex-WS endpoints in card, plan presets
- Beta banner scoped to the Quota Share page (functional-but-bugs-expected) with a
  pre-filled "open an issue" link (labels quota-share,beta). Page-only.
- Endpoints card now also surfaces POST /v1/responses (codex/github) and the
  codex-only WS /v1/responses line (the Responses-over-WebSocket proxy), each gated
  on the in-scope provider slug.
- planRegistry: seed xiaomi-mimo (4.1B-token weekly "lite" cap) and kimi-coding so the
  PoolWizard "Limite" step pre-fills a fair-share limit for these no-balance-API
  providers (fair-share enforces from the proxy's own token count, not an upstream
  balance — set the real cap manually in step 2).
- docs(API_REFERENCE): document the codex Responses-over-WebSocket endpoint.
- i18n en/pt-BR for all new keys.

Tracked in _tasks/features-v3.8.8/quota-share-key-redesign.plan.md (codex-WS config
toggle + per-provider balance fetchers + %-quota attribution are planned follow-ups).
2026-06-02 05:02:52 -03:00
diegosouzapw
11eb74c828 fix(quota-share): endpoints card default view shows real qtSd combos, not placeholders
The "Available endpoints" card's no-key (default) view generated representative
model ids from a hardcoded PREVIEW_MODELS_BY_PROVIDER map, so providers absent
from that map (claude, xiaomi-mimo, kimi-coding) rendered fake "model-a/b/c"
placeholders. It now fetches the REAL minted qtSd/* combos from /api/combos,
parses them (parseQuotaModelName), and groups by group → provider — falling back
to the placeholder map only when the fetch fails or returns nothing. The per-key
view already showed real models via /api/quota/keys/[id]/models; this aligns the
default view with it.

Verified on the local VPS: an exclusive key (share01) returns ONLY the real qtSd
models of its groups (claudao + chinas) and a non-quota key returns []. The
remaining /v1/models leak (non-quota keys still see qtSd among all models) is
tracked in the quota-key redesign plan.
2026-06-02 03:43:23 -03:00
Diego Rodrigues de Sa e Souza
fdbaae4734 fix(sse): stop infinite account-fallback loop on no-auth providers (#3061) (#3062)
No-auth / keyless providers (opencode, opencode-zen) returned synthetic
"noauth" credentials BEFORE honoring excludeConnectionIds, so the chat
account-fallback loop re-selected the same synthetic connection forever on
a persistent upstream error (e.g. the opencode public endpoint answering
401 "Model X is not supported"). The synthetic id has no DB row, so
markAccountUnavailable could not persist a cooldown to brake it — each
iteration wrote key-health + request logs immediately, growing the DB until
the disk filled (see @paraflu's "failure #320" trace in discussion #3038).

Honor the exclusion set in both synthetic-credential paths
(getProviderCredentials NOAUTH_PROVIDERS block + opencode-zen keyless
fallback): once "noauth" is already excluded, return null so the handler
stops after a single attempt. The happy path (nothing excluded -> synthetic
noauth) is preserved, so keyless access still works.

Closes #3061.

Tests (TDD): tests/unit/auth-noauth-fallback-loop-3061.test.ts — the two
exclusion cases failed before the fix and pass after; two happy-path guards
ensure first-selection synthetic noauth still resolves.
2026-06-02 03:29:10 -03:00
CitrusIce
7b87d2f169 fix(combo): align custom provider ids across creation and auth lookup (#3058)
* fix(combo): align custom provider ids

* fix(combo): tighten alias resolution

---------

Co-authored-by: minisforum <no@mail.com>
2026-06-02 02:18:06 -03:00
dangeReis
dee20ac665 fix(pii): preserve custom event names and apply stream PII sanitization fixes (#3059)
* fix(sse): defer enqueuing of event lines to align event names with data lines and prevent stop-signal event name misattribution

* fix(sse): preserve keep-alives and prevent pending event leakage on dropped chunks

* fix(sse): preserve pending event lines before other non-data lines and fix zero-window-size bypass

* fix(sse): defer lastEventLine update until after flush check to preserve previous event context on flush

* fix(sse): flush trailing pendingEventLine when stream closes

* fix(sse): preserve consecutive event lines without intervening data

---------

Co-authored-by: Ruslan Sivak <russ@ruslansivak.com>
2026-06-02 02:17:52 -03:00
diegosouzapw
66ddbb0f5a chore: remove Petals executor and tighten route typing
Remove the Petals executor from registration and exports.

Improve type safety by replacing broad any usage in MCP tool registration
with inferred types and documenting dynamic handler type limitations.

Add request validation for the agent bridge cert route and expand tests to
ensure switch buttons explicitly declare type="button", preventing implicit
form submissions.
2026-06-02 02:10:34 -03:00
diegosouzapw
3dca2bb3f1 fix(quota-share): hidden pools, delete-group UI, endpoints card (Anthropic + collapse)
Bugs found while testing the Quota Share engine on the local VPS:

- B1 hidden/stuck pools: pools created while the page group filter was "all"
  were persisted with group_id="all", matched no real group, and rendered
  nowhere — so they could not be seen, edited or deleted. PoolWizard now resolves
  the group id away from the "all" sentinel before POST/PATCH (falls back to the
  first real group / seed group-demo), and QuotaSharePageClient renders an
  "Ungrouped" recovery bucket so already-orphaned pools stay editable/deletable.
- B3 one-connection-per-pool made explicit: existingPoolConnectionIds now spans
  every member connection (not just the primary), and the wizard shows which pool
  an already-used connection belongs to instead of silently disabling it.
- B4 delete group: wired the missing UI control + handler (handleDeleteGroup,
  409-aware) — the backend DELETE handler + deleteGroup already existed. Hidden for
  "all" and the protected seed group-demo.
- B5a endpoints card now surfaces the native Anthropic POST /v1/messages line when
  a claude*/anthropic provider is in scope (previously only /v1/chat/completions).
- B5b endpoints card gained a collapse/minimize toggle (the card was too tall).

Source-scan tests + en/pt-BR i18n parity in quota-share-bugfixes-v388.test.ts.
The larger quota-key redesign (key type bound to a group, default-restricted with
opt-in normal-model access, recoverable keys, api-keys page layout) is planned
separately in _tasks/features-v3.8.8/quota-share-key-redesign.plan.md.
2026-06-02 02:07:24 -03:00
diegosouzapw
8386ab3084 fix: complete #3054 dead-provider removal (petals executor + stale tests)
#3054 ("remove 9 dead/unreachable free providers") removed the petals/nanobanana
configs, registry entries and validators but left dangling references that broke
the build and the unit suite on release/v3.8.8:

- open-sse/executors/petals.ts imported the deleted ../config/petals.ts
  (webpack "Module not found" → `next build` failed). Removed the executor, its
  registration + re-export in executors/index.ts, and the leftover
  `providerId === "petals"` branch in providerAllowsOptionalApiKey.
- Removed tests for the now-deleted providers: executor-petals.test.ts and
  poolside-provider.test.ts (REGISTRY.poolside was removed), and the petals /
  nanobanana validator assertions in provider-validation-specialty.test.ts,
  plus the stale petals catalog assertions in providers-page-utils.test.ts,
  proxy-connection-test.test.ts and providers-route-managed-catalog.test.ts.

The image/video/embed registries for nanobanana/replicate/nomic are real and
untouched — only the dead chat/api-key surfaces were removed. 146/146 affected
tests pass; typecheck / build clean.
2026-06-02 00:23:51 -03:00
dangeReis
c711ac62a7 fix(pii): preserve custom event names and apply stream PII sanitization fixes (#3056)
Co-authored-by: Ruslan Sivak <russ@ruslansivak.com>
2026-06-01 23:13:19 -03:00
Paijo
76d697bb88 chore: remove 9 dead/unreachable free providers (#3054)
* chore: remove 9 dead/unreachable free providers

Verified via HTTP probe — API endpoints return 000/404/empty:
- freetheai, enally, replicate, lepton, poolside, nomic
- astraflow, petals, nanobanana (phantom: catalog but no registry)

Also removed from: providerRegistry.ts, validation.ts,
staticModels.ts, imageValidation.ts, open-sse/config/petals.ts

* chore: remove dead astraflow providers

Remove astraflow and astraflow-cn (UCloud) — API endpoints unreachable.
Remaining dead providers (enally, freetheai, nanobanana, replicate,
lepton, petals, poolside, nomic) have working main sites but dead API
endpoints — need API keys. Will remove in follow-up.

* chore: remove 9 dead/unreachable free providers

Removed: freetheai, enally, replicate, lepton, poolside, nomic,
astraflow, petals, nanobanana

All verified as dead via live API probes (000/404/empty responses).
Cleaned from providers.ts, providerRegistry.ts, validation.ts,
staticModels.ts, and imageValidation.ts.

---------

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
2026-06-01 22:29:30 -03:00
terence71-glitch
08c70572fb fix(proxy): resolve registry assignments for combo and key levels (#3048)
* fix(proxy): resolve registry assignments for combo and key levels

* fix(proxy): guard registry scope level lookup
2026-06-01 19:47:46 -03:00
terence71-glitch
5350b5e7f5 fix(proxy): fail closed for OAuth usage account proxies (#3051) 2026-06-01 19:47:31 -03:00
diegosouzapw
8f0615fd04 fix(security): resolve 5 CodeQL alerts, document FPs, harden deploy skills
Real fixes (alerts auto-close on next scan):
- agentSkills/generator: escape backslash before double-quote in YAML
  frontmatter so a trailing backslash can't escape the closing quote
  (js/incomplete-sanitization #297/#298)
- usage: replace /\s*\(RESTRICTED\)\s*$/ with a non-backtracking literal +
  trim (js/polynomial-redos #275)
- i18n/request: skip __proto__/constructor/prototype in deepMergeFallback
  (js/prototype-pollution-utility #274)
- scripts/ad-hoc/nvidia diag: log key presence only, never key chars
  (js/clear-text-logging #273)
- tests: regression coverage for all three production fixes (YAML round-trip,
  proto-pollution guard, RESTRICTED strip + ReDoS timing)

Docs:
- ARCHITECTURE.md: executor count 45->55, OAuth modules 15->16, agy.ts in list

Deploy skills (deploy-vps-*-cc/-cx/-ag): add --legacy-peer-deps (npm v11
peer-dep resolver crashes on the omniroute tree) and replace the
"; pm2 start" that masked a failed install with a proper && chain.
2026-06-01 19:36:48 -03:00