Skip to content

bug: quota-exhausted 429 is retried for ~3 minutes with no UI feedback (session looks frozen) #1860

Description

@vinlee19

Environment

  • kimi-code 0.26.0 (darwin/arm64), provider type = "kimi" against https://api.moonshot.cn/v1, model kimi-k3

What happened

Our org ran out of token quota. From that moment every prompt — even a bare "hi" — made the TUI sit on the moon spinner for ~3 minutes and then fail. Users assumed the app was frozen; ps showed the process idle. Four consecutive turns reproduced identically.

Session log (timestamps UTC; one turn ≈ 189 s of silent retries):

11:51:30.355 INFO  llm request  turnStep=15.1
11:51:35.358 INFO  llm request  turnStep=15.1 attempt=2/10
...
11:54:34.426 INFO  llm request  turnStep=15.1 attempt=10/10
11:54:39.021 WARN  llm request failed  ... errorName=APIProviderRateLimitError
  errorMessage="429 Your account org-… <ak-…> is suspended due to insufficient balance,
  please recharge your account or check your plan and billing details" statusCode=429
11:54:39.037 ERROR turn failed  turnId=15

Captured provider response (curl, same account)

HTTP/2 429            (no retry-after header)
{"error":{"message":"Your account org-… <ak-…> is suspended due to insufficient balance,
  please recharge your account or check your plan and billing details",
  "type":"exceeded_current_quota_error"}}

Earlier the same account produced a different wording for the same error.type: "You exceeded your current token quota: <org-…> …, please check your account balance" — one structured type, at least two message variants, so the message text alone is not a reliable discriminator but the structured type is.

Why it behaves this way (from source)

  1. normalizeAPIStatusError classifies every 429 as a retryable APIProviderRateLimitError (packages/kosong/src/errors.ts) — the structured error.type/error.code parsed by the OpenAI SDK is dropped at the convertOpenAIError seam. A quota/balance 429 can never succeed on retry, yet it burns the full max_retries_per_step budget (10 attempts, 0.5→32 s backoff ≈ 3 minutes per turn).
  2. The engine does emit turn.step.retrying with full attempt/delay data, but the interactive TUI drops it (session-event-handler.ts: case 'turn.step.retrying': break;), so even legitimate transient-429 retries look like a frozen app. Print mode and the v2 activity view both surface this event; the shell TUI is the only consumer that doesn't.

Proposed fixes

  1. Fail fast on quota-exhausted 429 (exceeded_current_quota_error, insufficient_quota; billing-anchored message-pattern fallback), keeping transient rate-limit retry behavior unchanged — PR fix(kosong): fail fast on quota-exhausted 429 instead of retrying #1857.
  2. Show live retry progress in the TUI activity pane ("Rate limited (429) · attempt 3/10 · retrying in 12s") so genuinely transient retries are visible — companion PR incoming.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions