Skip to content

fix: OpenAI coach reported an empty credit balance as a network failure - #7

Merged
nseldeib merged 2 commits into
mainfrom
fix-openai-coach
Aug 3, 2026
Merged

fix: OpenAI coach reported an empty credit balance as a network failure#7
nseldeib merged 2 commits into
mainfrom
fix-openai-coach

Conversation

@nseldeib

@nseldeib nseldeib commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

A connected, valid OpenAI key returned "I couldn't reach Buddy just now. Check your connection and try again."

Root cause (measured, not inferred)

The account had no credits. OpenAI returns 429 for that — the same status as a burst rate limit — carrying type: "insufficient_quota" and code: "credit_balance_exhausted". We ignored both fields and reported every 429 as "Your OpenAI account is rate limited. Try again shortly." The app then flattened the 429 into the connection message.

Three layers, each pointing further from the truth, and the one thing the user could fix in a minute was never said. "Try again shortly" is advice that can never succeed: a burst limit clears in seconds, an empty balance never does.

Verified against the live account:

GET  /v1/models/gpt-5          -> 200          (the model was never the problem)
POST /v1/chat/completions      -> 429 insufficient_quota / credit_balance_exhausted
POST otterpace.com/api/coach   -> 429 "rate limited, try again shortly"   [before]
POST otterpace.com/api/coach   -> 402 "out of credits, add credits…"      [after]

Changes

An exhausted balance is now 402 insufficient_quota, with a message naming credits. 402 rather than 429 deliberately: the app routes 429 to a retry path, and retrying is exactly what cannot help. A genuine burst limit still maps to 429 rate_limited, so the distinction stays meaningful. Anthropic SDK errors are inspected the same way, and a provider that reports quota only in prose is matched on that.

CoachError.rateLimited had no case in the chat and fell through to the connection copy — sending the user to check their wifi over a provider-side throttle. It now says what it is.

Errors are no longer undiagnosable. throwForStatus mapped every non-401/429 status — including 400 and 404 — to "could not be reached". 400/404 now report model_unavailable and carry the provider's own error text through to the UI. A real 5xx still reads as an outage.

Reasoning tokens could consume the whole reply budget. max_completion_tokens bounds reasoning and visible output; at 1024 the model can spend it all thinking and return content: "" with finish_reason: "length". Floor is now 16k, and an exhausted budget raises token_budget_exhausted. This was a latent bug found while investigating — it was not the reported failure, and would have bitten as soon as credits existed.

Why tests didn't catch it

llm.test.ts stubs fetch and only asserted that 429 maps to rate_limited — it never distinguished why a 429 arrived. Added 9 backend + 6 Swift cases, including the verbatim insufficient_quota payload OpenAI returned.

  • Backend: 194 passing
  • Swift: 375 passing

Deploy note

Deployed to production; otterpace.com verified returning 402 with the actionable message.

The functional resolution for the reporting user is adding credits — no app update required. The improved wording ships with the app: existing installs receive the 402 but an older build renders it as the generic message.

🤖 Generated with Claude Code

A connected, valid OpenAI key returned "I couldn't reach Buddy just now. Check
your connection and try again." Neither the key nor the network was at fault:
every failure that was not a 401 collapsed into one generic message, so a
provider-side configuration fault was reported as a connectivity problem and
pointed the user at the one thing that was working.

Two problems, both fixed:

1. Undiagnosable. `throwForStatus` mapped every non-401/429 status — including
   400 (bad request) and 404 (unknown model) — to "could not be reached", and
   the app flattened everything except an invalid key into the connection copy.
   A misconfigured model and a real outage were indistinguishable from the
   chat. 400/404 now report `model_unavailable` and carry the provider's own
   error text, and the app renders that reason instead of the network line.
   A genuine 5xx still reads as an outage, so the new codes stay meaningful.

2. Reasoning tokens consumed the whole reply budget. `max_completion_tokens`
   bounds reasoning AND visible output, so 1024 — sized for a 2-4 sentence
   answer — could be spent before a single visible character was emitted. That
   returns content "" with finish_reason "length", which read as "no text" and
   surfaced as a 502. The floor is now 16k, and an exhausted budget raises
   `token_budget_exhausted` rather than hiding as an empty completion.

The existing tests stub fetch, so they never exercised the real token budget or
finish_reason and could not have caught this; the new cases pin both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
otterpace Ready Ready Preview Aug 3, 2026 10:00pm

The reported OpenAI outage was an account with no credits. OpenAI returns 429
for that — the same status as a burst limit — carrying type "insufficient_quota"
and code "credit_balance_exhausted". We ignored both and reported every 429 as
"Your OpenAI account is rate limited. Try again shortly."

That advice can never succeed. A burst limit clears on its own in seconds; an
empty balance never does. The app then flattened the 429 into "I couldn't reach
Buddy just now. Check your connection and try again." — so three layers each
pointed further from the truth, and the one thing the user could actually fix
in a minute was never said.

Verified against the live account:
  GET /v1/models/gpt-5                 -> 200 (the model was never the problem)
  POST /v1/chat/completions            -> 429 insufficient_quota
  POST otterpace.com/api/coach         -> 429 "rate limited, try again shortly"

An exhausted balance now raises 402 `insufficient_quota` with a message naming
credits, and the app renders it. 402 rather than 429 on purpose: the app routes
429 to a retry path, and retrying is exactly what cannot help. A genuine burst
limit still maps to 429 `rate_limited`, so the distinction stays meaningful.
Anthropic's SDK errors are now inspected the same way, and a provider that
reports quota only in prose is matched on that.

Also: CoachError.rateLimited had no case in the chat and fell through to the
connection copy, sending the user to check their wifi over a provider throttle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@nseldeib nseldeib changed the title fix: OpenAI coach failed with a misleading "check your connection" fix: OpenAI coach reported an empty credit balance as a network failure Aug 3, 2026
@nseldeib
nseldeib merged commit 969bfe7 into main Aug 3, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant