Skip to content

Handle AI usage cap gracefully in studio code - #3102

Merged
gcsecsey merged 7 commits into
trunkfrom
gcsecsey/ai-usage-cap-handling
Apr 21, 2026
Merged

Handle AI usage cap gracefully in studio code#3102
gcsecsey merged 7 commits into
trunkfrom
gcsecsey/ai-usage-cap-handling

Conversation

@gcsecsey

@gcsecsey gcsecsey commented Apr 15, 2026

Copy link
Copy Markdown
Member

Related issues

Related to STU-1555
Companion PR 211652-ghe-Automattic/wpcom

How AI was used in this PR

I used Claude to implement the changes and write tests. I reviewed and tested all changes manually.

Proposed Changes

When a user hit the AI usage cap while running studio code, the agent would appear to hang for minutes before eventually showing a generic error. The cause is two non-obvious behaviors of @anthropic-ai/claude-agent-sdk:

  1. The SDK retries non-2xx responses up to 10 times by default (via CLAUDE_CODE_MAX_RETRIES). Cap responses won't recover from a retry, so the user just waits through exponential backoff before seeing anything.
  2. The SDK only classifies HTTP errors when ANTHROPIC_BASE_URL points at api.anthropic.com. We point it at the WordPress.com proxy (https://public-api.wordpress.com/wpcom/v2/ai-api-proxy), so the SDK falls through to error: 'unknown' regardless of what shape the upstream 429 body has. The response body is forwarded into the assistant message as a text block, but SDKAssistantMessage.error never says 'rate_limit'.

This PR surfaces the cap and offers a path forward (/provider to switch to a personal Anthropic API key):

  • Fail fast on 4xx: apps/cli/ai/providers.ts sets CLAUDE_CODE_MAX_RETRIES=0 for the wpcom provider so the SDK stops retrying and returns control immediately.
  • Detect the cap from the SDK message: apps/cli/ai/ui.ts matches a studio_cap_exceeded marker (set by the backend in the 4xx response body) inside assistant content text blocks. Since the SDK forwards the upstream body, this is the only reliable signal we get through a proxy. Picking a marker we control instead of pattern-matching the upstream JSON shape keeps the contract explicit and survives any SDK debug-output changes.
  • Suppress redundant errors: after surfacing the cap message, the SDK still emits a trailing "Claude Code process exited with code 1" from the agent runner. handleAgentTurnError checks ui.hasErrorBeenSurfaced() and bails out so the user only sees one clear message. The "Done" / "Thought for Xs" indicators are also suppressed when the turn ended in a cap.

I also experimented with a pre-flight quota check. It would only benefit the already-capped case, while mid-turn caps, the realistic failure mode for long agent loops where token budgets get debited as the turn runs, are handled entirely by the in-turn marker detection. Adding a per-turn round trip for a marginal UX gain wasn't worth it.

The companion PR 211652-ghe-Automattic/wpcom makes the marker show up in the response body.

Testing Instructions

  • Follow the test instructions on 211652-ghe-Automattic/wpcom
  • Checkout this branch
  • Use curl 'https://public-api.wordpress.com/?amisandboxed' to confirm sandboxing is active
  • Build the CLI with npm run cli:build
  • Run the CLI agent: node apps/cli/dist/cli/main.mjs code (and log in if needed)
  • With the cap not yet exceeded: send any prompt and confirm the agent responds normally
  • Lower the cap on the backend, then send a prompt
  • Check that the agent does not hang, the cap message appears within a second or two
  • Check that you get the error message
  • Check that there is no trailing "Done" indicator and no "Claude Code process exited with code 1" line
  • Run /provider, switch to "Anthropic · API key", paste a key, and confirm subsequent prompts work
  • Switch back to "WordPress.com" and confirm the cap message appears immediately on the next prompt
Cap reached After switching to API key After switching back to WP.com
CleanShot 2026-04-17 at 15 04 14@2x CleanShot 2026-04-17 at 15 04 33@2x CleanShot 2026-04-17 at 15 04 44@2x

Pre-merge Checklist

  • Have you checked for TypeScript, React or other console errors?
@gcsecsey gcsecsey changed the title Handle AI usage cap (429) gracefully in chat assistant Apr 15, 2026
@gcsecsey gcsecsey changed the title Handle AI usage cap gracefully in chat assistant Apr 15, 2026
@gcsecsey gcsecsey closed this Apr 16, 2026
@gcsecsey
gcsecsey force-pushed the gcsecsey/ai-usage-cap-handling branch from f61f949 to 38792ef Compare April 16, 2026 15:45
When the WordPress.com proxy returns the studio_cap_exceeded marker,
detect it pre-flight via /studio-app/ai-assistant/quota and during a
turn via the SDK's forwarded error body. Surface a friendly cap
message with a /provider hint, suppress the trailing "Done" / SDK
exit error, and fail fast by setting CLAUDE_CODE_MAX_RETRIES=0 so
the agent doesn't hang through the SDK's default retry budget.
@gcsecsey gcsecsey reopened this Apr 16, 2026
The pre-flight check via /studio-app/ai-assistant/quota only helps the
already-capped case. Mid-turn caps — the realistic failure mode for
long agent loops — are handled by the studio_cap_exceeded marker
detection and CLAUDE_CODE_MAX_RETRIES=0 in the SDK env, so the
pre-flight is just an extra round trip on every turn for no UX gain.
@gcsecsey
gcsecsey marked this pull request as ready for review April 17, 2026 14:06
@gcsecsey
gcsecsey requested a review from a team April 17, 2026 14:06
@wpmobilebot

wpmobilebot commented Apr 17, 2026

Copy link
Copy Markdown
Collaborator

📊 Performance Test Results

Comparing f3acf5f vs trunk

app-size

Metric trunk f3acf5f Diff Change
App Size (Mac) 1490.96 MB 1490.96 MB +0.00 MB ⚪ 0.0%

site-editor

Metric trunk f3acf5f Diff Change
load 1905 ms 1861 ms 44 ms ⚪ 0.0%

site-startup

Metric trunk f3acf5f Diff Change
siteCreation 8125 ms 8100 ms 25 ms ⚪ 0.0%
siteStartup 4947 ms 4947 ms 0 ms ⚪ 0.0%

Results are median values from multiple test runs.

Legend: 🟢 Improvement (faster) | 🔴 Regression (slower) | ⚪ No change (<50ms diff)

@nightnei nightnei left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. I tried a few times and always was waiting for aroudn 200s.
  2. Also I got Claude Code process exited with code 1
    According to testing steps, it's not expected?
Screenshot 2026-04-20 at 19 01 00
@gcsecsey

Copy link
Copy Markdown
Member Author
  1. I tried a few times and always was waiting for aroudn 200s.

    1. Also I got Claude Code process exited with code 1
      According to testing steps, it's not expected?
Screenshot 2026-04-20 at 19 01 00

Thanks for testing @nightnei! Yes, neither the long wait nor the error message are expected. From the error message, it seems that you have the quota set on the backend correctly, so this might be something on the CLI side. 🤔

Strange, because in my testing, I got ~10s turnaround time from my sandbox, although I can reproduce the second error message being shown:

CleanShot.2026-04-21.at.10.53.12.mp4

I'll investigate further and fix this.

gcsecsey and others added 2 commits April 21, 2026 11:00
…ached

Detect the cap via the SDK's "API Error: 429" text prefix (provider-gated
to wpcom) so the detection survives backend body variations. Once the cap
message has been surfaced, skip the generic "Claude Code process exited
with code 1" error and the "hiccup on the server" retry prompt — retrying
won't recover a cap.
@gcsecsey

Copy link
Copy Markdown
Member Author

@nightnei the error message detection drifted slightly between the backend and the cli changes, so you were essentially seeing the trunk behavior. I fixed it now, and now I get the error message as expected when following the testing steps in the PR description:

CleanShot.2026-04-21.at.11.39.51.mp4

Could you please give this another look? Thanks!

@nightnei nightnei left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested, it works well now and LGTM 👍
Screenshot 2026-04-21 at 14 53 36

@gcsecsey
gcsecsey merged commit 35a5d5a into trunk Apr 21, 2026
10 checks passed
@gcsecsey
gcsecsey deleted the gcsecsey/ai-usage-cap-handling branch April 21, 2026 15:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

3 participants