Commit Graph

1243 Commits

Author SHA1 Message Date
diegosouzapw
e5392a7eaa fix(quota): pool PATCH prunes OLD group/provider combos before re-sync (no orphan qtSd/ on switch) 2026-06-01 02:35:26 -03:00
diegosouzapw
e694674851 feat(quota): pool PATCH accepts groupId + connectionIds, re-syncs combos on connection change 2026-06-01 02:28:21 -03:00
Diego Rodrigues de Sa e Souza
4b3987e190 Merge pull request #3022 from diegosouzapw/feat/quota-share-redesign
feat(quota): Pool Groups + correções de enforcement → v3.8.8 (+ authz peer-IP, screen fixes)
2026-05-31 22:05:18 -03:00
diegosouzapw
58a7d97b3f Merge branch 'release/v3.8.8' into feat/quota-share-redesign
# Conflicts:
#	src/lib/db/apiKeys.ts
#	src/lib/db/migrationRunner.ts
#	src/server/authz/policies/management.ts
#	src/server/authz/routeGuard.ts
#	tests/unit/route-guard-private-lan.test.ts
2026-05-31 21:08:30 -03:00
diegosouzapw
137d77cf12 feat(quota): group selector + grouped pool cards + wizard group pick (Interface A)
Task B9: QuotaSharePageClient fetches /api/quota/groups, renders a group bar
(select + New group + Rename group actions), and filters pool cards by the
selected group. PoolWizard adds a group picker in step 1 and POSTs groupId;
default pool name now uses provider slug instead of the raw connection
label/email. EditAllocationsModal adds a group allocation note. i18n parity
for 7 new quotaShare keys in en + pt-BR. PoolCreateSchema gains optional
groupId field. Test: tests/unit/quota-groups-ui.test.ts (21 source-scan
assertions, all green).
2026-05-31 19:52:00 -03:00
diegosouzapw
5d84a8fa7a test(quota): combo protection guard matches qtSd/ prefix 2026-05-31 19:33:24 -03:00
diegosouzapw
efa8f0af23 feat(quota): /api/quota/groups CRUD (rename re-syncs combos)
Add GET/POST /api/quota/groups and PATCH/DELETE /api/quota/groups/[id].
PATCH rename calls renameGroup then re-syncs quotaShared-* combos (via
dynamic-import syncQuotaCombos) for every pool in the group, since combo
names embed the group slug. DELETE maps the deleteGroup throw (protected
group-demo or pools still referencing the group) to 409 Conflict via
buildErrorBody — never 500. All handlers are management-gated via
requireManagementAuth and route all errors through buildErrorBody (Hard
Rule #12). 37-assertion source-scan test verifies auth, error sanitisation,
response shapes, Zod validation, PATCH re-sync wiring, and DELETE 409 logic.
2026-05-31 19:31:30 -03:00
diegosouzapw
eac24ca458 feat(quota): group-level allocations + group access enforcement
upsertAllocations now propagates allocation rows to every pool in the
same group. A key allocated to pool A in group G automatically gets
an allocation row in pool B (same group), so enforceQuotaShare finds
the row when the key calls pool B's model and applies fair-share.

No change to apiKeyPolicy Check 3 (already group-correct via B5
groupSlug logic) or enforce.ts pool-match (works once propagation
supplies the row).
2026-05-31 19:24:59 -03:00
diegosouzapw
e9cc53b17f feat(quota): key scope + /v1/models expand to the whole group (real group)
- `getPoolsByGroup(groupId)` in quotaPools.ts: SELECT all pools for a group,
  exposed as public API and re-exported from localDb.ts.
- `resolveQuotaKeyScope`: pool → group → all pools in group expansion.
  The returned `poolSlugs` field now holds GROUP slugs (quotaGroupSlug of the
  group name) instead of individual pool-name slugs, one per distinct group.
  Group slug included only when the group has ≥1 valid connection (group-level
  anyValidConnection gate). Deduplication: a key in 2 pools of the same group
  expands once.
- `filterModelsToQuotaPools` (quotaCombos.ts): already matches by
  `parsed.groupSlug` ∈ poolSlugs — no logic change needed, just test alignment.
- quota-key-resolve.test.ts: updated 4 tests that asserted pool-name slugs
  (testpoola2, poolmix, etc.) to assert the group slug ("groupdemo") — this is
  alignment to the B5 group semantics, not masking; the assertions were correct
  for the old pool-slug behavior and are now correct for the new group-slug
  behavior.
- quota-catalog-filter.test.ts: updated fixtures from old quotaShared-* format
  to qtSd/<groupSlug>/... (naming changed in B3, test was already broken before B5).
2026-05-31 19:15:16 -03:00
diegosouzapw
6d2e695882 fix(sse): stop Codex multi-account family-revocation cascade on quota-sync
The Quota / Providers dashboard (POST /api/usage/provider-limits ->
syncAllProviderLimits, GET /api/usage/[connectionId]) calls
refreshAndUpdateCredentials() per connection, in concurrent chunks. For
rotating-refresh providers (Codex/OpenAI share one Auth0 client_id) the
single-use refresh_token is rotated on every refresh; refreshing siblings
concurrently makes Auth0 revoke the whole token family (openai/codex#9648),
killing every account but the last with [403] <!DOCTYPE html>. On the affected
VM expires_at was persisted as ~0 so needsRefresh() was effectively always
true -> every codex account refreshed on every page visit -> guaranteed cascade.

Fix #1: refreshAndUpdateCredentials skips proactive refresh for rotating
providers (rotationGroupFor) and reuses the current access_token for the quota
fetch; genuine expiry is handled by the reactive, serialized 401 path.

Fix #2 (defense in depth): serializeRefresh inserts a settle gap between two
QUEUED sibling refreshes (default 2000ms, CODEX_REFRESH_SPACING_MS, '0' to opt
out) but releases a lone refresh immediately, adding no latency to the reactive
request path.

Tests: codex-quota-sync-no-proactive-refresh (skip + non-rotating guard),
refresh-serializer-spacing (default/opt-out + lone-vs-queued behavior).
2026-05-31 19:13:41 -03:00
diegosouzapw
fc6692925e feat(quota): group-aware quotaShared combos (qtSd/<group>/...) with provider-scoped prune
- resolvePoolForSync now resolves groupName via getGroupName(pool.groupId),
  falling back to pool.name when the group record is missing.
- Both quotaModelName calls in syncQuotaCombos use groupName instead of
  pool.name, so combos are named qtSd/<groupSlug>/<provider>/<model>.
- Prune is now scoped to group+provider: syncing pool A (openrouter) never
  deletes pool B (baidu) combos that share the same group.
- removeQuotaCombosForPool likewise scoped to group+provider.
- Updated quota-combos-sync, quota-combo-balancing, quota-combo-cli-providers
  tests for the new group-slug naming.
- New tests/unit/quota-combo-groups.test.ts: G1–G4 cover two-pool same-group
  naming, provider-scoped prune isolation, default-group fallback, and stale
  prune within the same group+provider.
2026-05-31 19:02:32 -03:00
diegosouzapw
cb1a18fa1a feat(quota): qtSd/<group>/<provider>/<model> model naming
Replace the old quotaShared-<poolSlug>-<provider>/<model> format with
qtSd/<groupSlug>/<provider>/<model>. Adds quotaGroupSlug() as the canonical
helper; keeps quotaPoolSlug() as a delegating alias so existing callers
(quotaKey.ts, quotaCombos.ts) compile without changes. parseQuotaModelName
now returns { groupSlug, provider, model }. Updates quotaCombos.ts,
apiKeyPolicy.ts, and all tests that referenced the old field name or the
old hardcoded prefix literal.
2026-05-31 18:48:41 -03:00
diegosouzapw
1eff658678 feat(quota): quotaGroups DB module (CRUD)
Add src/lib/db/quotaGroups.ts with createGroup/getGroup/getGroupName/
listGroups/renameGroup/deleteGroup; deleteGroup guards against non-empty
groups (throws /pools/i) and protects the 'group-demo' seed. Re-export
all functions + QuotaGroup type from localDb.ts. 15 unit tests in
tests/unit/quota-groups-crud.test.ts all passing.
2026-05-31 18:32:50 -03:00
diegosouzapw
727616b2c0 feat(quota): quota_groups table + pools.group_id (migration 087)
Introduces first-class quota Group entity: new quota_groups table,
quota_pools.group_id column, GroupDemo seed, backfill of existing
pools, and groupId in QuotaPool/PoolCreate/PoolUpdate with a
group-demo default. Migration runner gains an isSchemaAlreadyApplied
guard for version 087.
2026-05-31 18:28:12 -03:00
diegosouzapw
7cb77a9083 Merge PR 3018 into release/v3.8.8 2026-05-31 18:18:38 -03:00
Tentoxa
7b6007bc2d chore: bump Claude Code identity to 2.1.158 + sync beta flags from HAR captures
- Bump CLAUDE_CLI_VERSION / CLAUDE_CODE_VERSION from 2.1.146 to 2.1.158
  (matches claude-cli 2.1.158 captures)
- Add redact-thinking-2026-02-12 to the always-sent beta tier
  (present in Haiku, Sonnet, and Opus CC 2.1.158 captures)
- Add mid-conversation-system-2026-04-07 for Opus full-agent only
  (present in Opus capture, absent from Sonnet/Haiku)
- Remove afk-mode-2026-01-31 from heavy-agent beta flags
  (not present in any real Claude Code 2.1.158 capture)
- Add mid-conversation-system-2026-04-07 to ANTHROPIC_BETA_BASE
- Update CC-compatible bridge version strings
- Fix STEALTH_GUIDE.md outdated version + Stainless version (0.81.0 → 0.94.0)
- Update claude-beta-flags-2454 tests to match real CC behavior
2026-05-31 18:17:34 -03:00
Mcdowell Terence
029e4f6943 docs(docker): align memory default docs 2026-05-31 18:16:45 -03:00
diegosouzapw
ddd129f3f9 fix(quota): block per-key overage for countable units (real pool-total consumed)
Replace the saturation-signal approximation (globalUsedPercent × effectiveLimit,
which is always 0 for requests/tokens/usd) with store.poolConsumedTotal() so
the pool actually saturates and enforceQuotaShare returns "block" when the pool
total hits the effective limit for countable dimensions.

"percent" dimensions continue to use the upstream saturation signal, which is
the authoritative measure for provider-reported utilisation windows.
2026-05-31 18:15:11 -03:00
diegosouzapw
7ec85ce054 Merge PR 3015 into release/v3.8.8 2026-05-31 18:09:29 -03:00
diegosouzapw
254c8bf335 Merge PR 3012 into release/v3.8.8 2026-05-31 18:09:27 -03:00
diegosouzapw
98cc1df3b5 Merge PR 3000 into release/v3.8.8 2026-05-31 18:09:18 -03:00
Diego Rodrigues de Sa e Souza
a6de2a58c5 Merge pull request #2965 from soyelmismo/fix/oom-memory-leak-caches
fix(oom): prevent unbounded memory growth causing heap OOM crashes
2026-05-31 18:06:18 -03:00
Diego Rodrigues de Sa e Souza
fa038e069d Merge pull request #2964 from S0yora/feat/trae-solo-provider
feat(oauth): add Trae SOLO provider
2026-05-31 18:06:17 -03:00
Diego Rodrigues de Sa e Souza
bc46eba8f6 Merge pull request #2975 from xz-dev/feat/siliconflow-cn-endpoint-selector
feat(providers): add SiliconFlow endpoint selector
2026-05-31 18:06:11 -03:00
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
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
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
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
Mcdowell Terence
c455a360ab fix(proxy): ignore null Proxifly API entries 2026-05-31 15:21:21 +02: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