Commit Graph

4934 Commits

Author SHA1 Message Date
diegosouzapw
abb77879db feat(quota): QuotaStore.poolConsumedTotal — pool aggregate per dimension
Add poolConsumedTotal(poolId, dim) to the QuotaStore interface and implement
it in both SqliteQuotaStore and RedisQuotaStore so the enforce path can read
the real pool-wide consumption (sum across all keys) rather than the per-key
saturation signal, which is 0 for countable units and therefore never blocks.

- db/quotaConsumption.ts: add sumPoolDimension() — single SQL COALESCE(SUM)
  for curr and prev buckets across all api_key_id rows for a dimensionKey.
- localDb.ts: re-export sumPoolDimension.
- quota/types.ts: add poolConsumedTotal to QuotaStore interface (with JSDoc).
- sqliteQuotaStore.ts: implement using sumPoolDimension + existing
  slidingWindowEffective helper — one consistent sliding-window read.
- redisQuotaStore.ts: implement by fetching pool allocations from SQLite (F2),
  then issuing a single MGET for all (key, curr-bucket) and (key, prev-bucket)
  Redis keys, summing raw values, and applying the sliding-window formula once.
- tests/unit/quota-store-pool-total.test.ts: 4 tests (sum, pool isolation,
  unit isolation, peek-no-regression) all passing.
2026-05-31 17:57:02 -03:00
oyi77
57930bae93 test: add model resolver, stream collector, gemini helper tests
- model-resolver.test.ts: 21 tests (resolveProviderAlias, parseModel, normalizeCrossProxyModelId, getModelInfoCore)
- stream-payload-collector.test.ts: 12 tests (compactStructuredStreamPayload, buildStreamSummaryFromEvents, createStructuredSSECollector)
- gemini-helper.test.ts: 20 tests (tryParseJSON, extractTextContent, generateRequestId, convertOpenAIContentToParts, cleanJSONSchemaForAntigravity)
2026-06-01 03:42:36 +07:00
oyi77
1543a4ba77 test: add core, models, settings extended tests
- db-core-extended.test.ts: 27 tests (isNativeSqliteLoadError, getDbInstance, closeDbInstance, getDriverInfo, setAutoVacuum, runManualVacuum, runManagedDbHealthCheck, toSnakeCase, toCamelCase, objToSnake, rowToCamel, cleanNulls)
- db-models-extended.test.ts: 17 tests (sanitizeUpstreamHeadersMap edge cases, getModelCompatOverrides, getModelIsHidden, getModelUpstreamExtraHeaders)
- db-settings-extended.test.ts: 23 tests (getSettings, updateSettings, getPricing, updatePricing, resetPricing, getProxyConfig, setProxyConfig, getCacheMetrics, getCacheTrend, getLKGP)
2026-06-01 03:42:35 +07:00
oyi77
73075f9fb3 test: add executor base utils and usage provider tests
- executor-base-utils.test.ts: 20 tests (mergeUpstreamExtraHeaders, setUserAgentHeader, mergeAbortSignals)
- usage-providers.test.ts: 18 tests (getUsageForProvider for 15+ providers, error handling)
2026-06-01 03:42:35 +07:00
oyi77
54ca6ba9b6 test: add 5 new test files for DB modules and usage utils
- db-apiKeys-crud.test.ts: 54 tests (API key lifecycle, validation, caching)
- db-core.test.ts: 40 tests (core DB helpers, row conversion, encryption)
- db-domainState-crud.test.ts: 32 tests (domain state, circuit breakers)
- db-registeredKeys-crud.test.ts: 26 tests (key registration, rotation)
- usage-utils.test.ts: 43 tests (parseResetTime, quota snapshots, plan inference)

Total: 195 new test cases, all passing
2026-06-01 03:42:35 +07:00
diegosouzapw
be77a03aa0 fix(quota): await getQuotaStore() in enforce — quota never enforced/recorded (fail-open)
getQuotaStore() is async; enforce.ts used it without await, so store was a Promise
and store.peek/store.consume threw 'not a function' → enforceQuotaShare failed open
on every request and recordConsumption never wrote. Production quota was a silent
no-op (unit tests passed because they inject a sync mock store). Await it + guard.
2026-05-31 17:20:10 -03:00
oyi77
c6cc8bb334 fix(usage): export pure helper functions for unit testing
Adds 17 missing exports to the __testing object in usage.ts so
usage-utils.test.ts can import and test them. Also adds the
test file itself.

- toDisplayLabel, getClaudePlanLabel, createQuotaFromUsage
- getMiniMaxQuotaResetAt, isMiniMaxTextQuotaModel
- getMiniMaxSessionTotal, getMiniMaxWeeklyTotal
- createMiniMaxQuotaFromCount, getMiniMaxAuthErrorMessage
- getMiniMaxErrorSummary
- mapCodeAssistSubscriptionToPlanLabel
- mapCodeAssistTierIdToLabel
- mapSubscriptionTierStringToPlanLabel
2026-06-01 02:31:15 +07:00
diegosouzapw
10fa5e8903 feat(quota): protect quotaShared-* combos from manual edit/delete (system-managed)
Guard PUT and DELETE on /api/combos/[id] to return 409 when the target combo name
starts with QUOTA_MODEL_PREFIX; filter isHidden combos from the Combos page state
so quota-managed combos never appear as editable rows there.
2026-05-31 15:23:41 -03:00
diegosouzapw
cb45d9dfe9 fix(quota): prune deleted pool id from api_keys.allowed_quotas
deletePool now runs a SQLite json_each UPDATE inside the existing
transaction to remove the pool's id from every api_key's allowed_quotas
JSON array, preventing stale pool references after deletion.

Tests: tests/unit/quota-pool-delete-prune.test.ts (7 scenarios).
2026-05-31 15:17:01 -03:00
diegosouzapw
5acf6bd9cd feat(quota): default to equal split when allocation weights are unset (runtime + UI)
Runtime (enforce.ts): compute effectiveWeight = 100/N for each key when the
pool's total weight is 0, so pools with all-zero weights (newly created via
UI) are usable immediately without a re-save. Original non-zero weights are
unchanged.

UI (PoolWizard, EditAllocationsModal): addKey now recomputes all weights to
an equal split after adding a key, so saved pools store equal weights and
never persist all-zero allocations.

Tests: tests/unit/quota-equal-split.test.ts (7 scenarios).
2026-05-31 15:16:55 -03:00
Mcdowell Terence
aa6091aa14 fix(proxy): use connection proxy for OAuth refresh 2026-05-31 19:01:30 +02:00
diegosouzapw
5208cd5936 feat(quota): mask emails across the quota-share screen (EmailPrivacyToggle) 2026-05-31 13:17:28 -03:00
diegosouzapw
99f615c7e0 fix(quota): generate quotaShared-* combos for CLI/OAuth providers (use REGISTRY not PROVIDER_MODELS)
getProviderModelIds read PROVIDER_MODELS, which is empty for CLI/OAuth providers
(codex, kimi, claude, …) — so pools built from those providers produced ZERO
quotaShared-* combos and their quota keys saw no models. Read the provider REGISTRY
instead (the same source /v1/models uses), which covers all providers.
2026-05-31 13:03:44 -03:00
Diego Rodrigues de Sa e Souza
51d66eadee Merge PR 3005 into release/v3.8.8
fix(payload-rules): read DB-persisted rules when no in-memory override (#2986)
2026-05-31 11:34:08 -03:00
diegosouzapw
c2d7ac9359 fix(payload-rules): read DB-persisted rules when no in-memory override (#2986)
Payload rules are written to the DB (key_value settings.payloadRules) and
mirrored into an in-memory runtimeOverride. After a restart, if runtimeOverride
is null — the boot applyRuntimeSettings hook didn't run in this module instance,
or a separate bundle instance in the standalone Next.js build — getPayloadRulesConfig
fell back to the (usually empty) config/payloadRules.json file and returned no
rules, so saved rules appeared to vanish.

The persistence/apply path is correct on inspection (DB write+read both use the
'settings' namespace; the boot hook sets the override with force:true); the null
override is an environment/bundling effect. Make getPayloadRulesConfig read the
DB-persisted rules (via getCachedSettings, the source of truth) before the file
when no override is set, so saved rules deterministically survive a restart.

Closes #2986
2026-05-31 11:31:34 -03:00
diegosouzapw
8c11acaa33 feat(quota): per-pool usage-log endpoint + card 2026-05-31 11:31:20 -03:00
Diego Rodrigues de Sa e Souza
b1e1f5e5f6 Merge PR 3004 into release/v3.8.8
fix(models): honor per-model targetFormat for custom models (#2905)
2026-05-31 11:28:49 -03:00
diegosouzapw
18b615f5f0 feat(quota): show per-account upstream quota in the pool card
Adds a read-only AccountQuotaRow component that fetches cached quota data
from GET /api/usage/provider-limits (same endpoint as ProviderLimits.tsx)
and renders a compact per-connection quota summary (% remaining + reset
countdown) inside each PoolCard, below the dimensions section and before
the BurnRateChart. Fail-softs to "—" on error / empty / loading state.
2026-05-31 11:21:25 -03:00
diegosouzapw
d4045e74b5 fix(models): honor per-model targetFormat for custom models (#2905)
Custom models (added via the UI) on opencode-go / openai-compatible nodes always
routed as OpenAI-compatible because there was no per-model targetFormat: addCustomModel
didn't accept it, the API schema stripped it, and getModelTargetFormat (static-registry
only) never saw it — so a custom model needing the Anthropic Messages shape fell back to
the provider default 'openai'.

Thread an optional targetFormat through addCustomModel / replaceCustomModels /
updateCustomModel + the providerModelMutationSchema + the POST/PUT route, surface it from
getModelInfo (one combined custom-model metadata lookup alongside apiFormat), and use it
in chatCore's targetFormat resolution before the provider default.

Closes #2905
2026-05-31 11:17:52 -03:00
diegosouzapw
d490a30b58 feat(quota): pool budget = per-account limit × account count (summed balde)
A pool with N same-type connections now has an effective budget of
perAccountLimit × N per dimension. Only the limit fed to fair-share
scales — consumption (pool-keyed shared bucket) is unchanged.
2026-05-31 11:09:25 -03:00
diegosouzapw
4157fbe7a9 feat(quota): balance + failover across same-provider accounts (N-step fill-first combo)
Replace the (connId × modelId) upsert loop in syncQuotaCombos with a
model-grouped build: one combo per model with ALL connections as steps
and strategy "fill-first", fixing the collision where a second same-provider
connection overwrote the first (last upsert won, only one account ever used).
2026-05-31 11:02:18 -03:00
Diego Rodrigues de Sa e Souza
687c28474d Merge PR 3002 into release/v3.8.8
fix(providers): route Pollinations to gen.pollinations.ai/v1 (#2987)
2026-05-31 10:59:37 -03:00
diegosouzapw
17b58f8f5f fix(providers): route Pollinations to gen.pollinations.ai/v1 (#2987)
Pollinations retired the legacy text.pollinations.ai host, which now returns
404 'This is our legacy API' for all models. The registry primary baseUrl (and
the executor's hardcoded fallback) still pointed at it; gen.pollinations.ai/v1
is the current OpenAI-compatible gateway (it was already listed as the
secondary fallback). Make gen the sole/primary endpoint and align the executor
test.

Closes #2987
2026-05-31 10:56:21 -03:00
diegosouzapw
2521c09119 feat(quota): one provider per pool (block mixed-type) — server guard + wizard filter
- Add assertSingleProvider() helper in quotaPools.ts: queries provider_connections
  with DISTINCT provider WHERE id IN (...), throws if >1 provider detected.
- Call guard early in createPool (when connectionIds.length > 1) and updatePool
  (when input.connectionIds.length > 1), before any DB writes.
- Add lockedProvider useMemo in PoolWizard.tsx: derived from first selected
  connection; availableConnections filtered to same provider once locked.
- Show wizardSingleProviderNote i18n hint in step 1 when lockedProvider is set.
- Add wizardSingleProviderNote to en.json + pt-BR.json (parity).
- New test: tests/unit/quota-pool-single-provider.test.ts (4 cases).
- Update tests/unit/quota-multiprovider.test.ts: all D2 tests now use two
  same-provider connections (both PROVIDER_A/openrouter) — the tests were
  exercising connection-plumbing (scope, enforce membership, combo fan-out),
  not mixed-provider behavior per se; updated to remain valid under Task 3 rule.
2026-05-31 10:53:02 -03:00
diegosouzapw
bbc4c575e0 feat(quota): explain key-enable + exclusive behavior in concept card 2026-05-31 10:41:22 -03:00
diegosouzapw
767fad3d5c feat(quota): 2-column responsive grid for pool cards 2026-05-31 10:38:18 -03:00
Diego Rodrigues de Sa e Souza
009ad13a91 Merge pull request #2965 from soyelmismo/fix/oom-memory-leak-caches
fix(oom): prevent unbounded memory growth causing heap OOM crashes
2026-05-31 10:32:29 -03:00
Diego Rodrigues de Sa e Souza
c4a359ef5f Merge pull request #2964 from S0yora/feat/trae-solo-provider
feat(oauth): add Trae SOLO provider
2026-05-31 10:31:40 -03:00
Mcdowell Terence
c455a360ab fix(proxy): ignore null Proxifly API entries 2026-05-31 15:21:21 +02:00
S0yora
6ff3238e8d fix(oauth): address Trae review feedback (loopback origin, stream cleanup, scripts)
- Restrict the /authorize postMessage and the modal listener to the loopback
  origin pair (localhost + 127.0.0.1) instead of "*"/single-origin. The dashboard
  runs on localhost while Trae forces the callback onto 127.0.0.1, so a single
  window.location.origin target silently dropped the success message (popup never
  closed). Addresses CWE-359 without breaking the cross-loopback flow.
- TraeExecutor: cancel the SSE reader on completion, abort upfront when the caller
  signal is already aborted, and accept string elements in array message content.
- Move ad-hoc dev scripts to scripts/ad-hoc/ per the repo style guide.
2026-05-31 16:20:32 +03:00
Diego Rodrigues de Sa e Souza
b2e1e6c1e9 Merge PR 2999 into release/v3.8.8
fix(codex): drop image_generation for free-plan accounts (#2980 spin-off)
2026-05-31 09:48:13 -03:00
diegosouzapw
d0a87970c6 fix(codex): drop image_generation for free-plan accounts (#2980 spin-off)
Free-plan Codex accounts (workspacePlanType === "free", from the OAuth id_token)
cannot run the server-side image_generation hosted tool, but the Codex CLI
injects it into every Responses request — causing an upstream 400. normalizeCodexTools
now drops image_generation when the connection is a free-plan account; paid plans
keep it. Mirrors CLIProxyAPI's isCodexFreePlanAuth guard.

Found during the #2980 (Codex OAuth compat) analysis against CLIProxyAPI.
2026-05-31 09:45:38 -03:00
Mcdowell Terence
8e9b3e217a fix(deps): remove proxifly package dependency 2026-05-31 14:43:11 +02:00
Diego Rodrigues de Sa e Souza
81bf13e866 Merge PR 2994 into release/v3.8.8
fix(dashboard): resolve custom provider display name across surfaces (#2968)
2026-05-31 09:36:56 -03:00
diegosouzapw
8151c46139 fix(dashboard): resolve custom provider display name across surfaces (#2968)
Custom providers (openai-compatible-<uuid> / anthropic-compatible-<uuid>) showed
the raw UUID id instead of the user-given node name in the active-requests panel,
proxy logger, and home-page provider topology. Only RequestLoggerV2 resolved it
(via a local helper + /api/provider-nodes fetch).

Extract the resolver into a shared util (src/shared/utils/providerDisplayLabel.ts,
unit-tested), reuse it in RequestLoggerV2, and apply it (with a provider-nodes
fetch) in ActiveRequestsPanel, ProxyLogger, and the HomePageClient topology so all
surfaces show the user-defined provider name.

Closes #2968
2026-05-31 09:34:16 -03:00
Diego Rodrigues de Sa e Souza
933121b082 Merge PR 2993 into release/v3.8.8
fix(docker): honor OMNIROUTE_MEMORY_MB heap limit in standalone launcher (#2939)
2026-05-31 09:30:11 -03:00
diegosouzapw
4b6d6c7670 fix(docker): honor OMNIROUTE_MEMORY_MB heap limit in standalone launcher (#2939)
The Docker image bakes NODE_OPTIONS=--max-old-space-size=256, and the standalone
launcher (scripts/dev/run-standalone.mjs, the Docker CMD) spawned 'node
server.js' without overriding it — so the server inherited the 256 MB cap and
OOMed randomly under load or with a large SQLite DB. `omniroute serve` already
honored OMNIROUTE_MEMORY_MB but the Docker path did not.

Add a shared resolveMaxOldSpaceMb() helper (OMNIROUTE_MEMORY_MB, default 512,
clamped [64,16384]) and have the launcher append --max-old-space-size to the
child NODE_OPTIONS (a trailing flag wins, overriding the baked 256 without
clobbering other flags). Update the .env.example doc to reflect the 512 default.
2026-05-31 09:15:55 -03:00
Diego Rodrigues de Sa e Souza
d0e5b97d10 Merge PR 2991 into release/v3.8.8
fix(docker): add 'web' compose profile for web-cookie providers (#2832)
2026-05-31 09:13:57 -03:00
diegosouzapw
bc7ab44f74 fix(docker): add 'web' compose profile for web-cookie providers (#2832)
Web-cookie providers (gemini-web, claude-web, claude-turnstile) need
Playwright/Chromium, which only ships in the runner-web image. The default
docker-compose.yml had no profile targeting runner-web, so 'docker compose up'
ran the base image and those providers failed with
'Executable doesn't exist at .../ms-playwright/chromium...'.

Add an 'omniroute-web' service (target runner-web, image omniroute:web,
profile 'web') and document it in the header. Validated with
'docker compose --profile web config'.
2026-05-31 09:09:27 -03:00
Diego Rodrigues de Sa e Souza
2d627a3433 Merge PR 2990 into release/v3.8.8
fix(routing): honor client reasoning.effort for gpt-5.5 + route suffixed variants to codex (#2877)
2026-05-31 09:08:10 -03:00
diegosouzapw
f5d74cd76d fix(routing): honor client reasoning.effort for gpt-5.5 + route suffixed variants to codex (#2877)
(A) For a Codex-only account, a bare gpt-5.5 Responses request was rerouted to
codex with the model hardcoded to gpt-5.5-medium (chatHelpers.ts). The Codex
executor reads a model-name suffix as an explicit modelEffort that, per #2331,
overrides the client's reasoning.effort — so a genuine reasoning.effort=xhigh
was silently demoted to medium. Keep the bare gpt-5.5 id (the connection
fallback still supplies the default effort); the executor precedence is
untouched, so #2331 stays intact.

(B) gpt-5.5-xhigh/-high/-low misrouted to the openai provider (only bare gpt-5.5
was codex-preferred), so codex-only users got 'No credentials for provider:
openai'. Add the suffixed variants to CODEX_PREFERRED_UNPREFIXED_MODELS so they
infer codex before the /^gpt-/ → openai fallback.

Closes #2877
2026-05-31 09:05:35 -03:00
Diego Rodrigues de Sa e Souza
923b8fe14f Merge PR 2989 into release/v3.8.8
fix(sse): remove duplicate const settings in handleChatCore (release-blocker)
2026-05-31 09:03:51 -03:00
diegosouzapw
1a701292b6 fix(sse): remove duplicate const settings in handleChatCore
handleChatCore declared `const settings = cachedSettings ?? await
getCachedSettings()` twice in the same function scope — the consolidated
"fetch once, reuse" const near the top, plus a duplicate added alongside the
per-key stream-default-mode feature. esbuild/tsx rejected the same-scope
redeclaration ("The symbol 'settings' has already been declared"), which made
every unit test that imports chatCore fail to transform and broke the
production build.

Remove the duplicate; reuse the earlier consolidated `settings`. Add an
import-guard regression test.
2026-05-31 09:00:30 -03:00
diegosouzapw
cf9a78ad32 fix(quota): unwrap pool usage response so quota-share page renders pools with allocations
GET /api/quota/pools/[id]/usage returns { usage: snapshot }, but usePoolUsage
stored the whole wrapper, leaving usage.dimensions undefined. StackedAllocationBar
then dereferenced usage.dimensions[dimensionIndex] (undefined[0]) and crashed the
entire quota-share page for any pool that has allocations. Unwrap data.usage in the
hook + defensively guard usage.dimensions?.[i] / dim.perKey ?? [] in the bar.
2026-05-31 08:39:05 -03:00
Diego Rodrigues de Sa e Souza
86a22d64c7 Merge pull request #2975 from xz-dev/feat/siliconflow-cn-endpoint-selector
feat(providers): add SiliconFlow endpoint selector
2026-05-31 08:26:47 -03:00
Diego Rodrigues de Sa e Souza
ef58d83adc Merge pull request #2981 from Lion-killer/i18n/uk-ua-ui-menu
fix(i18n): translate Ukrainian (uk-UA) menu and UI strings
2026-05-31 08:26:13 -03:00
Diego Rodrigues de Sa e Souza
ca7756e990 Merge pull request #2984 from terence71-glitch/perf/provider-proxy-overlay-lookups
perf(proxy): parallelize provider proxy overlay lookups
2026-05-31 08:25:29 -03:00
Mcdowell Terence
0e35292e0e perf(proxy): parallelize provider proxy overlay lookups 2026-05-31 12:46:20 +02:00
diegosouzapw
559252aee4 docs(changelog): credit #2954 SessionPool modular (thanks @oyi77) 2026-05-31 07:07:45 -03:00
diegosouzapw
d22564df4d fix(session-pool): round-robin fingerprints in SessionFactory
createSession() used rotator.random() despite its docstring saying 'next
available fingerprint'. Random draws collide in a small profile pool,
producing duplicate fingerprints (and duplicate sess-<fp>-<ms> ids) across
a warm pool — flaky tests and look-alike sessions. Switch to rotator.next()
so each pooled session gets a distinct fingerprint, as documented.
2026-05-31 07:05:21 -03:00