- Fix `handoffProviders` and `nextBody` unknown property TS errors in contextHandoff
- Correct fallback payload parsing from responses `input` array in combo service
Move chat pipeline validation, circuit breaker execution, proxy
resolution, logging, and session header handling into dedicated
helpers to keep the SSE handler smaller and easier to verify.
Also fix shared API option precedence, rebuild skill version caches
after deletions, ignore api.trycloudflare.com false positives, and
add rate-limit manager test flush/reset hooks for deterministic
coverage.
Expand integration and unit coverage across chat routing, auth,
cloud sync, skills, executors, streaming, DB helpers, proxy handling,
and provider/model utilities.
- Add proxy support to all OAuth flows (authorization, token exchange, import)
- Add proxy support to token refresh operations for all providers
- Add proxy support to model synchronization
- Initialize global fetch proxy patch at server startup
- Use Proxy Registry with priority: Provider Proxy → Global Proxy → Direct
- Fix Global Proxy display in settings UI to show proxy from Proxy Registry
Changes:
- open-sse/services/tokenRefresh.ts: Add proxyConfig parameter to all refresh functions
- src/sse/services/tokenRefresh.ts: Resolve proxy before calling refresh functions
- src/app/api/oauth/*/route.ts: Use resolveProxyForProvider for OAuth flows
- src/app/api/providers/[id]/models/route.ts: Add proxy support for model sync
- src/instrumentation-node.ts: Initialize proxy patch at startup
- src/app/api/settings/proxy/route.ts: Read Global Proxy from Proxy Registry
- src/lib/db/proxies.ts: Export resolveProxyForProvider
- src/lib/localDb.ts: Re-export resolveProxyForProvider
- src/models/index.ts: Re-export resolveProxyForProvider
15 files changed, 405 insertions(+), 240 deletions(-)
Co-authored-by: growab <growab@users.noreply.github.com>
Replace inline `provider === "gemini"` checks in chatCore.ts and auth.ts
with shared hasPerModelQuota() and lockModelIfPerModelQuota() from
accountFallback.ts. Also adds max-cooldown preservation to lockModel()
to prevent race conditions from overwriting longer lockouts.
isModelLocked() was called to lock models on 429 but never checked
when selecting connections. Requests to a locked model would still
route to it, causing unnecessary repeated 429s.
Gemini AI Studio enforces per-model quotas. Previously a 429 on
gemini-2.5-pro would mark the entire connection as credits_exhausted,
blocking all models on that API key.
Three-layer fix:
- chatCore: lock model only (not connection) for RATE_LIMITED and
QUOTA_EXHAUSTED errors from Gemini
- auth: early-return with model-only lockout before terminal status
check, so credits_exhausted is never set on the connection
- rateLimitManager: use model-scoped limiter keys for Gemini so the
Bottleneck queue pauses only the affected model, not the connection
- chat: skip markAccountExhaustedFrom429 for Gemini (per-model quotas)
Gemini AI Studio has per-model quota limits. When one model hits its
quota (429), other models on the same API key may still be available.
- Use lockModel() for 429s on Gemini (model-only lockout, connection
stays active for other models)
- Skip markAccountExhaustedFrom429 for Gemini (prevents deprioritizing
the entire connection in credential selection)
- Apply model-only 404 lockout for Gemini too (deprecated/unavailable
models shouldn't disable the provider)
- Strip reasoning_text/reasoning_content from assistant messages in
GitHub executor to prevent upstream 400 'Invalid signature in
thinking block' errors
- Sync refreshed copilotToken to top-level credentials in
checkAndRefreshToken so buildHeaders() uses the fresh token
instead of the stale one
- Include providerSpecificData in refreshCredentials return value
so onCredentialsRefreshed persists the new token to DB correctly
- Standardize cooldown fallback to 2 min (COOLDOWN_MS.rateLimit) in both
chatCore and auth.ts instead of inconsistent 60s/undefined
- Return 504 Gateway Timeout instead of 200 OK when SSE collection times
out, with finish_reason "length" to signal incomplete response
A 429 from one Antigravity model was marking the entire provider connection
as rate-limited, blocking ALL other models on the same account. This happened
in two places: chatCore's error classification (primary) and
markAccountUnavailable (secondary). Both now use model-only lockModel() for
passthrough providers instead of connection-wide rateLimitedUntil.
Also adds:
- Bare Pro model ID normalization (gemini-3-pro → gemini-3-pro-low)
matching OpenClaw convention
- Internal model exclusion list for quota display, matching CLIProxyAPI
- Update model list to match CLIProxyAPI filtered models (remove stale
gemini-2.5-pro, claude-sonnet-4-5, claude-sonnet-4, gemini-2.0-flash;
sort alphabetically)
- Extend 404 model-only lockout to passthrough providers so one missing
model doesn't lock out the entire Antigravity connection
- Always use streaming upstream endpoint (generateContent causes upstream
400 for some models that internally convert to OpenAI format with
stream_options); collect SSE into JSON for non-streaming clients
- Fix unlimited quota models showing 0% by checking q.unlimited before
recalculating percentage
* feat: auto-disable banned accounts setting with UI toggle
Add a configurable setting to automatically disable provider accounts
that return permanent/terminal errors (403 banned, ToS violation, etc.)
Changes:
- open-sse/services/accountFallback.ts: extend ACCOUNT_DEACTIVATED_SIGNALS
with AG-specific ban messages ('verify your account', 'service disabled
for violation')
- src/app/api/settings/auto-disable-accounts/route.ts: new GET/PUT endpoint
for the setting (enabled bool + threshold int)
- src/shared/validation/schemas.ts: updateAutoDisableAccountsSchema
- src/sse/services/auth.ts: in markAccountUnavailable(), capture result.permanent
from checkFallbackError() and — when autoDisableBannedAccounts is enabled and
backoffLevel >= threshold — set isActive=false on the connection
Default: disabled (backward-compatible). Enable via Settings UI or PUT
/api/settings/auto-disable-accounts { "enabled": true, "threshold": 3 }
Fixes: antigravity accounts with 403/Verify-your-account errors being
retried indefinitely in the rotation pool.
Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
* fix: address reviewer comments for auto-disable (use getCachedSettings, immediate disable on permanent bans)
---------
Co-authored-by: oyi77 <oyi77@github.com>
Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
Adds intelligent cache control preservation for Claude Code clients:
- New cacheControlPolicy.ts module with detection logic:
- isClaudeCodeClient(): Detects Claude Code via User-Agent
- providerSupportsCaching(): Checks provider (claude, anthropic, zai, qwen)
- isDeterministicStrategy(): Identifies priority/cost-optimized strategies
- shouldPreserveCacheControl(): Main policy decision
- Cache control is preserved when:
1. Client is Claude Code (detected via User-Agent)
2. Provider supports prompt caching
3. Request routing is deterministic:
- Single model requests (always)
- Combo with priority or cost-optimized strategy only
- Updated translator to accept preserveCacheControl option
- Updated chatCore and chat handler to propagate combo strategy
- Added comprehensive unit tests (24 tests)
Non-deterministic combo strategies (weighted, round-robin, random, etc.)
continue to use OmniRoute's managed caching strategy.
Refs: #cache-control-preservation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When all combo models are exhausted (502/503), OmniRoute now checks for
a globalFallbackModel setting and attempts one last request through it
before returning the error. Settings stored in key_value table, no
migration needed.
High backoffLevel (up to 15) persisted permanently in the DB after a burst of 429s. The account health score dropped to zero (100 - 15*10 = -50), causing the account selector to never pick the account again. Only a successful request could reset backoffLevel via clearAccountError, but the account was never selected — creating a deadlock.
Now, during account selection, any non-terminal connection whose rateLimitedUntil has passed gets its backoffLevel reset to 0 and testStatus restored to active. The DB update is fire-and-forget to avoid blocking the hot path.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Add stripModelPrefix boolean setting that, when enabled, strips
provider prefixes (e.g. openai/, anthropic/) from incoming model
names and re-resolves the bare model name using existing heuristics.
This allows tools to send prefixed model names while OmniRoute
handles provider routing at the proxy layer.
- Add stripModelPrefix to settings validation schema (Zod)
- Check setting in getModelInfo() after custom node matching fails
- Falls through to normal resolution on error or when disabled
- Backward compatible: opt-in, default behavior unchanged
Add model-pattern → combo mapping feature that automatically routes requests
to specific combos based on model name patterns (glob matching).
Implementation:
- New migration 010: model_combo_mappings table with pattern, combo_id, priority
- DB module with CRUD + resolveComboForModel() using glob-to-regex matching
- getComboForModel() in model.ts: augments getCombo() with pattern fallback
- chat.ts: replaced getCombo() → getComboForModel() at routing decision point
- API endpoints: GET/POST /api/model-combo-mappings, GET/PUT/DELETE by [id]
- ModelRoutingSection.tsx: dashboard UI with inline add/edit/toggle/delete
- Integrated into Combos page
- 15 new unit tests (glob matching, priority ordering, disabled filtering)
- Full test suite: 923/923 pass
Examples:
claude-sonnet* → code-combo
claude-*-opus* → frontier-combo
gpt-4o* → openai-combo
gemini-* → google-combo
Resolves: #563
Cherry-pick from codex/omniroute-fixes-20260324:
- Replace MCP singleton transport with per-session architecture for Streamable HTTP
- Fix Claude passthrough via OpenAI round-trip normalization
- Add detectFormatFromEndpoint() for endpoint-aware format detection
- Support raw code#state in OAuth modal for Claude Code remote auth
- Expose cloudConfigured/cloudUrl/machineId in settings API
- Switch docker-compose.prod.yml target to runner-cli
- Add 3 new tests for round-trip and detectFormat
PR: #562
- Remove apiKey===null heuristic (too broad — could match cloud providers
with non-standard auth). Use URL-based detection only.
- Guard local 404 branch with provider && model check — if either is null,
fall through to standard connection lockout (safer behavior).
- Document LOCAL_HOSTNAMES as module-load-time constant (restart required).
- Document PROVIDER_PROFILES.local as intentionally not yet wired.
When a local inference backend (oMLX, Ollama, LM Studio) returns 404
for an unknown model, OmniRoute previously locked the entire connection
for 2 minutes — blocking all valid models on that connection.
This fix introduces local provider detection and changes the 404
behavior for local providers:
- Model-only lockout (5s) instead of connection-level lockout (2min)
- Connection stays active — other models continue working immediately
- Detection via URL heuristic (localhost/127.0.0.1) + apiKey===null fallback
- Configurable via LOCAL_HOSTNAMES env var for Docker setups
Also fixes a pre-existing bug where the model parameter was not passed
to markAccountUnavailable() from chat.ts, preventing per-model lockouts
from working at all.
Changes:
- Add isLocalProvider(baseUrl) helper in providerRegistry.ts
- Add COOLDOWN_MS.notFoundLocal (5s) and PROVIDER_PROFILES.local
- Add local 404 branch in markAccountUnavailable() in auth.ts
- Pass model param to markAccountUnavailable() in chat.ts (bug fix)
Tighten the helper signatures added for recovered provider cleanup.
This removes the new any-typed recovery parameters called out in
review without broadening the PR into unrelated auth typing work.
Clear recovered provider error metadata after successful
credentialed requests in non-chat API routes as well.
Add route-level regression tests covering a Response-based
success path and a result-object success path.
Pass errorCode, lastErrorType, and lastErrorSource through the
runtime credentials object so clearAccountError can clear stale
provider error metadata after a real successful request.
Also update the regression test to use getProviderCredentials,
matching the production call path.
Clear errorCode, lastErrorType, and lastErrorSource when an
account recovers so provider state returns to a fully clean
active status.
Add a focused regression test for recovered-account cleanup.
Thanks @rexname (Maulana Hasanudin)! 🎉
Codex account quota policy (5h/weekly) with auto-rotation is now merged. Highlights:
- Per-account policy toggles (5h + weekly ON/OFF) in the Provider dashboard
- Accounts automatically skipped when enabled quota window reaches 90% threshold
- Auto re-eligibility when resetAt timestamp passes (no manual intervention needed)
- Side-effect free `getQuotaWindowStatus` getter design
- Safe partial merge of `codexLimitPolicy` on provider updates
Merged on top of main (v2.5.0) with no conflicts. Analytics label fix (#356) included. Thanks for the excellent quality and the 2-commit cleanup round! 🙏