Skip to content

feat(claude-cli): stop when a request is charged to usage credits - #96

Merged
k3dom merged 2 commits into
masterfrom
claude-cli-usage-credits
Oct 26, 2026
Merged

k3dom merged 2 commits into
masterfrom
claude-cli-usage-credits

Conversation

@k3dom

@k3dom k3dom commented Oct 26, 2026

Copy link
Copy Markdown
Owner

What and why

Two changes to @pi-plugins/claude-cli, taken from a comparison with earendil-works/pi-claude-code:

Usage-credit guard. Until now the plugin could quietly bill Claude usage credits (formerly "extra usage") once a plan limit ran out, or for models and modes that always use them. Claude Code sends a rate_limit_event with isUsingOverage for every response. The plugin used to stop reading at message_stop and never saw that event.

  • The plugin now holds message_stop back until Claude Code's rate_limit_event, its result or the end of its output. It ignores any stream events that come after message_stop.
  • If isUsingOverage is true, the turn fails with an error that tells the user how to stay on plan limits or how to allow credits.
  • A rejection before any response that asks to charge usage credits ("out of extra usage", "third-party apps now draw from your extra usage", …) becomes its own error.
  • Both error messages contain "billing", so pi's isRetryableAssistantError won't retry them.
  • PI_CLAUDE_CLI_ALLOW_USAGE_CREDITS=1 turns the guard off. It is read with Config.Boolean in the layer.

README. I removed the claim that using the subscription this way "stays within Anthropic's terms of service". In its place is a note: Anthropic announced in May 2026 that claude -p would be billed separately, paused that in June, and claude -p still draws from plan limits for now. There's also a new "Usage credits" section.

Verification

  • I ran real Claude Code 2.1.302 with --include-partial-messages. rate_limit_event arrives about 10 ms after message_stop and before result. That holds for a text reply and for a tool_use reply too, where Claude Code's tool attempt has already failed before message_stop. No second API request is made.

  • Through ClaudeCli.stream with real Claude Code (Haiku), I compared the time from the last content event to done, with and without this change, on a tool-use turn: 73/54 ms on master and 88/55 ms on this branch. The extra wait is negligible.

  • pi --no-extensions -e plugins/claude-cli --model claude-cli/claude-haiku-4-5 -p … loads the plugin and answers.

  • New tests:

    • a response charged to credits fails and pi won't retry it
    • the opt-out lets it through
    • a rejection asking for credits, which carries a 429 that pi would otherwise retry, is not retried
    • a response still completes when Claude Code exits without reporting its limits

    The fake's replies now end with a rate_limit_event and a result, as the real CLI's do.

  • ci passes.

Review findings skipped

The correctness review found nothing. I fixed all the standards findings except one. The stream_event handler stays a plain function that returns Effect.succeed(Option.match(...)), because written with Effect.fnUntraced it has no yield and oxlint's require-yield rejects it.

Assumptions and open questions

  • Each claude process serves exactly one request, so its first response always triggers a rate_limit_event, since Claude Code emits one when the info changes. If it ever doesn't, the response still completes on result or exit, just without the check.
  • The opt-out test builds the service with allowUsageCredits: true rather than setting the env var. Effect's default ConfigProvider takes a snapshot of process.env on first use, so the module-level runtime can't pick the variable up partway through the test run.
  • A response that was charged to credits has already been billed when the plugin stops. The guard stops pi from continuing the turn and sending more requests, like pi-claude-code does.

@k3dom
k3dom merged commit 4a8d06d into master Oct 26, 2026
2 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