fix: repair Claude API requests and report rejections accurately - #32
Merged
Conversation
Three defects made every AI request invalid on the wire:
- Json defaults to encodeDefaults = false, so ClaudeImageSource's `type`
and `media_type` were dropped and image blocks went out as bare
{"data": "..."}. explicitNulls is disabled alongside it so the unused
member of a content-block union is omitted rather than sent as null.
- claude-sonnet-4-20250514 was retired on 2026-06-15, so the model no
longer resolves. Move to claude-sonnet-5.
- Thinking is on by default from Sonnet 5 onward and shares the
max_tokens budget with the response. These calls parse the reply as
JSON, so a truncated body breaks decoding; disable thinking to keep
the full 1024 tokens for the answer.
A live credit-balance failure masked all three, so they surface only
once billing is resolved. Tests assert the serialised request shape.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A 400 fell into the catch-all branch and was reported as "Unexpected
error (400). Please try again.", which is misleading: the most common
cause is an exhausted credit balance, and no amount of retrying fixes
it. The user is told to do the one thing that cannot work.
Relay the API's own message for 400 instead. It is written for end
users ("Your credit balance is too low to access the Anthropic API.
Please go to Plans & Billing...") and covers malformed requests too.
This puts the existing but unused ClaudeErrorResponse to work, and
falls back to the status code when the body will not parse.
Also log the error body, mirroring YouCamApiClient.ensureSuccess, since
nothing recorded why a request failed. The body carries no credentials.
Note: relayed messages are English regardless of app locale, unlike the
hardcoded strings around them. Localising needs error *types* plumbed
through to the UI rather than strings; left as follow-up.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Summary
Investigating a reported HTTP 400 from the Claude API turned up three separate defects. The immediate cause was an exhausted credit balance, but the app reported it as
Unexpected error (400). Please try again.— advice that cannot work, since retrying never restores credit. Fixing the message then revealed two further bugs that the billing failure had been masking: every request we send is malformed, and the model we target no longer exists.Reproduced end-to-end on device via the Gaps tab (
GapsViewModel:49setsisAiMode = hasKeyand loads on entry, so it issues a real request with no photo needed):Topping up credits is still required to actually use the AI features — that part is account-side and outside this PR. These changes ensure that once billing is resolved the requests are valid, and that any future rejection explains itself.
Changes
Jsondefaults toencodeDefaults = false, soClaudeImageSource'stypeandmedia_typedefaults were silently dropped and image blocks went out as bare{"data":"..."}.explicitNulls = falseis set alongside it so the unused member of a content-block union is omitted rather than sent as an explicitnull.claude-sonnet-4-20250514was retired on 2026-06-15; it no longer resolves. Nowclaude-sonnet-5.max_tokensbudget with the response. These call sites parse the reply as JSON, so a truncated body breaks decoding — disabling it keeps the full 1024 tokens for the answer.ClaudeErrorResponseto work; falls back to the status code when the body will not parse. 401/429/5xx branches are unchanged.YouCamApiClient.ensureSuccess— nothing previously recorded why a request failed. The body carries no credentials.ClaudeApiClientTest(5 tests) asserting the serialised request shape and the error-message mapping.Test plan
./gradlew :shared:testAndroidHostTest— 5 new tests, all passing. They were written against the unmodified client first and failed as expected, confirming each defect was real rather than assumed; the pre-fix body was"source":{"data":"AQID"}with notypeormedia_type../gradlew detekt— passes.Unexpected error (400). Please try again.Checklist
./gradlew detektpassesNotes for review
ClaudeApiClientand picks these fixes up unchanged; it is left unchecked above only because it was not run.🤖 Generated with Claude Code