The redeem failure counter used INCR followed by Expire(24h) on every
attempt, so each failed attempt refreshed the key TTL and the window
never actually closed for a user who kept retrying within 24h.
- Count attempts in a fixed 10-minute window via a small Lua script
(INCR, then PEXPIRE only when the key is created or has no TTL), which
is atomic and also repairs keys that lost their TTL.
- Move the counter to the redeem:ratelimit:v2: key space so pre-existing
24h counters do not seed the new window.
- Rename the service threshold to redeemMaxFailedAttempts (30) to reflect
that it bounds attempts per window, not per hour.
Public redeem behavior is unchanged apart from the window semantics:
the check fails open when the counter read fails, and a failed
increment never masks the underlying domain error.
filterSchedulerExtra is an allowlist and never carried openai_passthrough /
openai_oauth_passthrough, while credentials.model_mapping is preserved in full.
Candidate filtering (ListSchedulableAccounts) reads that trimmed projection
(sched:meta:<id>), so there a passthrough account looks exactly like a
whitelist-mode one: Account.IsModelSupported falls back to the model_mapping
whitelist and the account is dropped as model_not_supported. Forwarding, which
hydrates the full account from sched:acc:<id>, still honors passthrough - hence
"testing the account directly succeeds, but the gateway reports no available
accounts supporting model X".
The practical impact is that a stale leftover whitelist silently shrinks the
candidate pool of accounts that were explicitly configured to let the upstream
decide. When no account in the group lists the requested model, the group is
fully dark for it even though every account can serve it.
This is not a regression of #4936: that fix corrected the predicate
(Account.IsModelSupported short-circuits passthrough before the mapping check),
this one restores the input the predicate is fed. The projection allowlist
predates #4936 by three and a half months and has never carried the flag, so
the fix simply never took effect on the snapshot path.
Add both keys to the allowlist, plus a regression test that round-trips the
projection through JSON (the real sched:meta write/read path) and asserts the
model gate still lets a non-whitelisted model through for a passthrough account.
Claude-Session: https://claude.ai/code/session_01LwvyJVV2sdqiu41Z9JDNUq
OpenAI/Composite groups can now reject requests that exceed the max
reasoning effort instead of always rewriting them down to the ceiling.
Existing groups keep the previous auto-downgrade behavior.
Public groups have always been bindable by every user: CanBindGroup returned
true for any non-exclusive group, and user_allowed_groups only ever carried the
exclusive groups an admin had granted. Admins had no way to hand a single user
a subset of the public groups short of converting a group to exclusive, which
changes it for everyone already using it.
A user now carries restrict_public_groups. While it is false, which is the
default and what every existing row migrates to, nothing changes: every public
group stays bindable. Once an admin turns it on for a user, that user's public
groups are narrowed to the ones listed in user_allowed_groups, the same table
that already gates exclusive groups.
The flag is an administrative control, so it rides on the admin user DTO only
and leaves the shape of the end-user endpoints alone.
The model plaza filter honours the flag as well, so a restricted user is not
shown groups they would be refused when binding a key. Anonymous visitors have
no user record and keep the previous view.
Enforcement rides on the existing CanBindGroup choke point, so it covers key
creation, key updates, and per-request authorization together. An API key bound
to a group that is later withdrawn stops working at request time rather than
lingering as a key that can be listed but not used.
The admin dialog gains a toggle over the public group list. Turning it off
re-checks every public group, so an admin cannot save a list that reads as
restrictive while the restriction itself is disabled.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>