Repository navigation
feat(server): optional API-only port - #3206
Open
MuhammadUsamaMX wants to merge 6 commits into
Open
MuhammadUsamaMX wants to merge 6 commits into
MuhammadUsamaMX wants to merge 6 commits into
Conversation
429 fallback currently locks an account for a fixed exponential backoff (getQuotaCooldown: base 2s … max 5m), ignoring the reset window the provider actually reports. So an account whose quota resets in 30m is retried every 5m (wasted 429s and fallback churn), while one that resets in seconds is held out far too long. Parse the provider's own reset signal from the 429 body/headers and use it as the cooldown instead: - Google/Antigravity RetryInfo "retryDelay": "1976.365s" (relative s) - OpenAI/Codex "resets_at" / "reset_at" (epoch s|ms) - OpenRouter / RateLimit headers "X-RateLimit-Reset" (epoch s|ms) - Retry-After: N (seconds) resolveCooldownMs() prefers the parsed reset, floors it at the static backoff (never retry sooner than before) and caps it at MAX_RATE_LIMIT_COOLDOWN_MS so a bogus/huge value can't wedge an account. Cap raised 30m -> 6h to accommodate real multi-hour resets (e.g. Codex). No signal present -> unchanged static backoff.
Add a per-provider retry-delay option so operators can set the cooldown applied when an account hits a rate-limit/error and the provider reports no reset window of its own. - Dashboard: a "Retry delay" selector on each provider page (Auto / 15s / 30s / 1m / 2m / 5m / 10m / 30m / 1h), saved to the new `retryDelayByProvider` setting via the existing /api/settings PATCH. - markAccountUnavailable honors it as a fallback only: a provider-reported reset (retryDelay / Retry-After / resets_at / X-RateLimit-Reset) is always used and never shortened. When the provider reports nothing, a configured value sets the lock duration; with "auto", free/free-tier provider pools cap the static backoff at 60s so a small account set recovers within its per-minute window instead of all locking for ~5m at once. Builds on the provider-reset-aware cooldown (uses parseProviderResetMs).
NVIDIA NIM enforces a per-API-key requests-per-minute limit. Crossing it
returns 429, which parks that account on a cooldown, so a burst can knock the
whole pool out for minutes at a time even though the accounts are healthy.
Count our own requests per account and skip an account that has used its
minute budget, so the provider never sees the 429 in the first place. When
every account for a provider is at its cap the request reports the wait like
any other rate-limit instead of failing hard.
The cap is per provider and editable from Providers -> [id] ("RPM / account",
next to Retry delay): blank uses DEFAULT_PROVIDER_RPM (40 for nvidia, unset
elsewhere) and 0 means unlimited, stored in settings.rpmByProvider.
Counters are in-process, which is the right scope for a single gateway; a
multi-instance deployment would need shared state.
An API key was only ever on or off. Anything sharing a 9router instance --
a second tool, a teammate, a script -- could spend the whole account pool and
reach every model, with no way to bound it short of revoking the key.
Give each key its own controls, enforced before any upstream call:
- RPM / TPM: sliding one-minute windows per key; over the limit returns 429
with the seconds until capacity returns.
- Max budget + budget period ("1d", "30d", "1mo"): spend accrues from the
cost already computed for usage history, so the number shown in the
dashboard is the number enforced. The window rolls over on its own.
- Model allowlist: empty means every model, otherwise anything not listed is
rejected with 403.
- Expiry: the key stops working at the configured time.
- Priority: stored and shown, for capacity reservation to build on.
All of it is optional -- existing keys migrate to NULL, which every check
reads as no limit, so behaviour is unchanged until a limit is set.
Enforcement lives in one place (services/keyPolicy.js) called from the chat
handler, so every inbound path applies the same rules. Token and spend
accounting hangs off saveRequestUsage, which already has the key and the cost.
Rate-limit counters are in-process, matching the existing per-account cap.
…on mobile The allowlist was a comma-separated text field, so a typo silently pinned a key to a model that does not exist and the mistake only showed up as a 403 on the next request. Offer the models the endpoint can actually serve instead, and show the picks as removable chips. Also drop the limits grid to a single column on small screens and let long model ids truncate rather than widen the row.
The dashboard and the OpenAI-compatible endpoint share one port, so exposing the endpoint to anything -- a tunnel, a mesh network, another host -- exposes the dashboard with it. The only alternative was a reverse proxy in front. Set API_PORT to serve /v1, /v1beta, /responses and /codex on a second port. Everything else 404s there, so the dashboard and its /api/* routes are simply not reachable on it. API_HOSTNAME controls the bind and defaults to loopback, so widening it is always deliberate. The main port is unchanged and still serves both, so this is opt-in: unset API_PORT and nothing differs.
MuhammadUsamaMX
force-pushed
the
feat/api-only-port
branch
from
August 10, 2026 14:40
808140e to
f3ab6f4
Compare
golamrabbi696
added a commit
to golamrabbi696/EzRouter
that referenced
this pull request
Aug 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The dashboard and the OpenAI-compatible endpoint share a single port. Exposing the endpoint to anything — a tunnel, a mesh network, another host — exposes the dashboard along with it, including
/api/*, which can read and write provider credentials. The only way to separate them today is to put a reverse proxy in front and filter by path.Change
Set
API_PORTand 9router serves/v1,/v1beta,/responsesand/codexon a second listener. Everything else returns 404 there, so the dashboard and its/api/*routes are not reachable on that port at all.API_HOSTNAMEcontrols the bind and defaults to127.0.0.1, so widening it is always a deliberate act.API_PORTunset and nothing differs.Verification
Running with
API_PORT=20129:Startup logs
▲ API-only listener on http://0.0.0.0:20129 (/v1, /v1beta, /responses, /codex), and a bind failure is reported without taking the main server down.