Repository navigation
Feature: multi-endpoint upstream fallback (retry a secondary LLM endpoint when the primary is rate-limited or down) #85
Description
Feature: multi-endpoint upstream fallback (retry a secondary LLM endpoint when the primary is rate-limited or down)
Problem
The proxy forwards every request to exactly one upstream LLM endpoint (llm.api_base + llm.api_key + llm.model_id). When that endpoint fails, _forward_to_llm_openai (skillclaw/api_server.py) retries the same endpoint up to 6 times with exponential backoff and then returns 502 Bad Gateway to the agent.
This hurts most with account-level quotas, where retrying is pointless by definition. Real example (z.ai / GLM coding endpoint):
08:00:14 429 {"error":{"code":"1308","message":"Usage limit reached for 5 hour.
Your limit will reset at 2026-08-30 09:16:13"}}
08:00:14 upstream LLM error (attempt 1/6), retrying in 1.3s
08:00:16 attempt 2/6 ... 429 (same body)
...
08:00:48 attempt 6/6 ... 429 (same body)
08:00:48 upstream LLM error → 502 Bad Gateway
Six identical retries burn ~34 seconds and the caller still ends up with a 502 — a scheduled job (e.g. a cron-driven post) simply fails even though a second key/endpoint was available. This is the same failure family as #24 (transient upstream errors), which was closed as "not our bug", but with a fallback endpoint it would have been a non-event.
Current behavior
skillclaw/api_server.py,_forward_to_llm_openai: singleapi_base,max_retries = 6, on exhaustion raisesHTTPException(502).- The only built-in failover is OpenRouter-specific:
openrouter.fallback_modelsaddsmodels: [primary, ...]to the request body, which delegates failover to OpenRouter itself. Nothing equivalent exists for a plain OpenAI-compatible upstream (z.ai, DeepSeek, vLLM, etc.), and it can't express "another endpoint/key for the same or different provider". - Config plumbing is a fixed 1:1 bridge:
ConfigStore.to_skillclaw_config()(skillclaw/config_store.py) mapsllm.api_base/api_key/model_idontoSkillClawConfig.llm_*fields (skillclaw/config.py).
Proposal
Support an ordered list of upstream endpoints. Try the primary; when it is exhausted (or when the error is a quota/limit error where retrying the same account is meaningless), fail over to the next entry; only after the last entry fails return 502.
Config sketch (YAML, consumed by ~/.skillclaw/config.yaml / skillclaw config llm.fallbacks[0].api_base ...):
llm:
provider: custom
api_base: https://api.z.ai/api/coding/paas/v4
api_key: sk-primary
model_id: glm-5.2
fallbacks:
- api_base: https://api.z.ai/api/coding/paas/v4 # same provider, second account
api_key: sk-backup
model_id: glm-5.2
- api_base: https://openrouter.ai/api/v1 # or a completely different provider
api_key: sk-or
model_id: vendor/modelImplementation notes:
- The change is naturally contained: the
(api_base, key, model_id)triple is already assembled inside_forward_to_llm_openai; the single-endpoint loop becomes the inner loop, and the endpoint list becomes the outer loop. Streaming does not need separate handling — the proxy always sendsstream: falseupstream and re-assembles SSE itself. - New config keys must be added in three places:
SkillClawConfigdataclass, the_DEFAULTS/bridge inConfigStore, and the retry loop. - Quota-aware fast-fail: on HTTP 429 whose body matches a usage-limit error (or that carries a
Retry-Afterfar in the future), skip the remaining retries for that endpoint and move to the next one immediately. Retrying the same account inside the limit window only adds latency (34 s in the example above). - Respect
Retry-Afterfor the final fallback decision only if cheap; otherwise current exponential backoff is fine. - The response's
modelfield should reflect the endpoint that actually served the request (agents normally useproxy.served_model_name, so this is cosmetic but useful for debugging). - Optional follow-ups (same pattern):
evolve_server/core/llm_client.pyon the server side, and the Codex Responses path (_forward_to_llm_responses). The chat path alone would already cover the Hermes/OpenClaw integrations.
Alternatives considered
- OpenRouter as primary with
fallback_models: works, but locks you into OpenRouter and only switches models, not endpoints/accounts. - LiteLLM router in front of the proxy (
fallbacksconfig): works, but adds a whole extra service to deploy next to skillclaw, and the fallback logic is ~30 lines away from where the request already is.
Environment
- Source:
mainas of 2026-08-31 (client proxy, Docker deployment, Hermes integration). - Upstream in the incident:
api.z.aiOpenAI-compatible endpoint, 429 / code 1308 (5-hour usage quota).
Activity
Upper9527 commentedon Oct 21, 2026
Confirmed against current main: the chat forwarding path retries a single upstream, and OpenRouter model fallback does not provide a general endpoint/key fallback list. Keeping this as an enhancement request.
A complete implementation needs ordered per-endpoint credentials/model settings, bounded total retry time, explicit retryable status handling (including 429/Retry-After), and tests that a success stops fallback and credentials never cross endpoint boundaries. For a future true-streaming path, fallback must stop once response bytes have been sent. Chat and native Responses behavior should be documented separately; this is not implemented by the existing retry loop.
Upper9527 commentedon Oct 21, 2026
已在 #100 合并可配置的 Chat 上游故障切换:支持最多 3 个备用端点,连接错误或 408/429/500/502/503/504 时切换;每个端点使用自己的模型和凭据。全部尝试共享 6 次上限与总超时(包含退避),长期配额窗口不会无限等待。中英文 README 已补充 YAML 示例和范围。
新增 23 项测试通过,覆盖鉴权隔离、故障切换、配额、重试预算和超时。原生 Responses、Bedrock 和独立 evolve-server 不使用该列表;该 PR 解决的是这里提出的代理 Chat 上游重试/备用节点需求。