fix: OpenAI coach reported an empty credit balance as a network failure - #7
Merged
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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
force-pushed
the
fix-openai-coach
branch
from
August 3, 2026 21:59
84e5f92 to
cb75729
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
429for that — the same status as a burst rate limit — carryingtype: "insufficient_quota"andcode: "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:
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 to429 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.rateLimitedhad 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.
throwForStatusmapped every non-401/429 status — including 400 and 404 — to "could not be reached". 400/404 now reportmodel_unavailableand 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_tokensbounds reasoning and visible output; at 1024 the model can spend it all thinking and returncontent: ""withfinish_reason: "length". Floor is now 16k, and an exhausted budget raisestoken_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.tsstubsfetchand only asserted that 429 maps torate_limited— it never distinguished why a 429 arrived. Added 9 backend + 6 Swift cases, including the verbatiminsufficient_quotapayload OpenAI returned.Deploy note
Deployed to production;
otterpace.comverified 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