calm-artist-7206
09/09/2026, 5:04 AMfresh-machine-37269
09/09/2026, 5:05 AMfresh-machine-37269
09/09/2026, 5:10 AM75c480c8ā¦) was created at 043927, but every LLM call immediately failed with:
litellm.RateLimitError: OpenAIException ā {"error":{"type":"usage_limit_reached",
"message":"The usage limit has been reached","plan_type":"team", ...}}
The agent retried 4 times (043930, 043939, 043955, 044028) before giving up, which left the conversation in an error state. The reviewer automation (main.py _check_conversation_completion) sees any terminal `error`/`stuck` status and posts that generic "ā ļø OpenHands PR Reviewer encountered a problem ⦠(status: error)" comment ā which is exactly what appeared at 045242, with no review text because the agent never got a single successful completion.
So this is an OpenAI team-plan usage limit (usage_limit_reached), not a code, CI, or PR-content failure. The limit was set to reset at ~05:48 UTC that day, so the review should simply be re-triggered once quota is available.
(As a side note, the same usage_limit_reached errors were also firing on other conversations around 02:49 UTC that day, so it was a fleet-wide quota issue, not specific to this PR.)calm-artist-7206
09/09/2026, 5:13 AMfresh-machine-37269
09/09/2026, 5:13 AMgpt-5.6-sol profile ā model openai/gpt-5.6-sol, running on the OpenAI subscription (Codex backend).
That's confirmed two ways:
⢠The reviewer automation builds its agent from GET /api/settings ā agent_settings.llm, which is set to openai/gpt-5.6-sol (auth_type: subscription, subscription_vendor: openai, `base_url: https://chatgpt.com/backend-api/codex`).
⢠The server log at 04:38:32 (right before the review conversation was created at 04:39:27) shows [Profile Store] Loaded profile gpt-5.6-sol.
The usage_limit_reached / plan_type: team error in the logs is the OpenAI subscription rate limit, which matches this profile (not the deepseek-v4 profile that's the agent-canvas UI default).calm-artist-7206
09/09/2026, 5:15 AMfresh-machine-37269
09/09/2026, 5:17 AMOpenHands/software-agent-sdk, and it's designed for exactly this case. Here's what I found and my take.
The mechanism
openhands-sdk/openhands/sdk/llm/fallback_strategy.py defines FallbackStrategy. It triggers fallback on a fixed set of transient exceptions, and RateLimitError is explicitly in that list:
_LLM_FALLBACK_EXCEPTIONS = (
APIConnectionError,
RateLimitError,
ServiceUnavailableError,
LiteLLMTimeout,
InternalServerError,
LLMNoResponseError,
)
The incident error (litellm.RateLimitError: usage_limit_reached) maps directly to that trigger. On an LLM, you set:
llm.fallback_strategy = FallbackStrategy(
fallback_llms=["glm"], # profile names in the profile store
profile_store_dir="...", # defaults to ~/.openhands/profiles
)
and the SDK will load those named profiles from LLMProfileStore, try them in order, and ā on success ā merge the fallback's cost/token metrics into the primary.
Your GLM profile already exists
~/.openhands/profiles/glm.json is present in the profile store (model openhands/glm-5.2, with an encrypted API key), so fallback_llms=["glm"] would resolve without creating anything new. (deepseek-v4 ā openhands/deepseek-v4-pro is also there.)
So: yes, it's possible to configure. The gpt-5.6-sol profile/settings LLM just needs a fallback_strategy field pointing at glm.
Caveats / my honest assessment
1. It only fires after retries are exhausted. The active LLM has num_retries: 5, retry_multiplier: 8.0, min wait 8s / max 64s. In the incident, litellm made 4 attempts before giving up ā so you'd see a meaningful delay (potentially a couple minutes) before the fallback even kicks in. If you want near-instant cutover on usage_limit_reached, the fallback path as-written still pays the retry backoff first.
2. It's per-call, not "sticky." Each new request restarts from the primary (gpt-5.6-sol), so under a sustained quota outage the agent would keep hammering the primary + backoff + fallback on every call ā slow and a bit noisy, though it would work.
3. Where it has to live matters for this automation. The PR reviewer builds its agent from GET /api/settings ā agent_settings.llm and passes that dict straight into POST /api/conversations. So the fallback_strategy needs to be on the active/settings LLM (i.e. in gpt-5.6-sol / the activated profile), not just sitting in a side profile.
4. Credential/auth asymmetry. Primary is a Codex subscription (auth_type: subscription), fallback glm is api_key auth. Functionally fine, but the fallback model will behave slightly differently (tool-calling fidelity, output style, reasoning), and the subscription path goes through create_subscription_llm_from_config, which I'd want to confirm preserves fallback_strategy end-to-end before trusting it in prod.
5. No nested fallback chains ā the fallback LLM has its own fallback_strategy disabled during the attempt, so you can't chain gpt-5.6-sol ā glm ā deepseek in a single call; it's a single level per request.
Bottom line: technically yes and low-effort ā add a fallback_strategy: {"fallback_llms": ["glm"]} to the active LLM profile. It'll catch exactly this RateLimitError. The main UX cost is the retry backoff before failover, and you'd be trading a code-quality regression risk (subscription ā api-key model difference) for availability. Worth doing, ideally paired with a lower num_retries (or a shorter backoff) on the primary so the cutover happens promptly rather than after ~2 minutes of retrying.calm-artist-7206
09/09/2026, 5:25 AMusage_limit_reached or what this case exactly is. then add the fallback profile in the profiles/fresh-machine-37269
09/09/2026, 5:54 AM