Skip to content

fix: repair Claude API requests and report rejections accurately - #32

Merged
jvsena42 merged 2 commits into
mainfrom
fix/claude-api-request-and-errors
Jul 27, 2026
Merged

fix: repair Claude API requests and report rejections accurately#32
jvsena42 merged 2 commits into
mainfrom
fix/claude-api-request-and-errors

Conversation

@jvsena42

Copy link
Copy Markdown
Owner

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:49 sets isAiMode = hasKey and loads on entry, so it issues a real request with no photo needed):

[ClaudeApi] HTTP 400 body={"type":"error","error":{"type":"invalid_request_error",
"message":"Your credit balance is too low to access the Anthropic API.
Please go to Plans & Billing to upgrade or purchase credits."}}

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

  • Serialise the full image source. Json defaults to encodeDefaults = false, so ClaudeImageSource's type and media_type defaults were silently dropped and image blocks went out as bare {"data":"..."}. explicitNulls = false is set alongside it so the unused member of a content-block union is omitted rather than sent as an explicit null.
  • Move off a retired model. claude-sonnet-4-20250514 was retired on 2026-06-15; it no longer resolves. Now claude-sonnet-5.
  • Disable thinking. Thinking is on by default from Sonnet 5 onward and shares the max_tokens budget 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.
  • Relay the API's own reason for a 400. Puts the already-defined but unused ClaudeErrorResponse to work; falls back to the status code when the body will not parse. 401/429/5xx branches are unchanged.
  • Log error bodies, mirroring YouCamApiClient.ensureSuccess — nothing previously recorded why a request failed. The body carries no credentials.
  • Add 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 no type or media_type.
  • ./gradlew detekt — passes.
  • On device: opened the Gaps tab and confirmed the screen now shows "Your credit balance is too low to access the Anthropic API. Please go to Plans & Billing to upgrade or purchase credits." in place of the previous Unexpected error (400). Please try again.
  • The malformed-payload and retired-model fixes could not be verified against the live API, since the credit-balance failure is rejected before either is evaluated. They are covered by unit tests only, and warrant a re-check on device once credits are added.

Checklist

  • ./gradlew detekt passes
  • Tested on Android
  • Tested on iOS
  • Updated documentation (if applicable)

Notes for review

  • Shared-module code only — no UI was touched, so the usual "update both Android and iOS" rule does not apply here. iOS consumes the same ClaudeApiClient and picks these fixes up unchanged; it is left unchecked above only because it was not run.
  • Relayed messages are English regardless of app locale, unlike the hardcoded strings around them. Localising properly means plumbing error types through to the UI rather than strings — a larger refactor, deliberately left as follow-up. The tradeoff seemed worth it: an accurate English message beats a localised but actively wrong one.

🤖 Generated with Claude Code

jvsena42 and others added 2 commits July 27, 2026 18:53
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>
@jvsena42
jvsena42 enabled auto-merge July 27, 2026 21:56
@jvsena42
jvsena42 merged commit f6fbec1 into main Jul 27, 2026
1 check passed
@jvsena42
jvsena42 deleted the fix/claude-api-request-and-errors branch July 27, 2026 22:01
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