Skip to content

fix(pi): Claude Code billing rejection and missing claude-opus-5 - #147

Open
unspecd-dev wants to merge 5 commits into
cortexkit:mainfrom
unspecd-dev:fix/pi-claude-code-billing
Open

fix(pi): Claude Code billing rejection and missing claude-opus-5#147
unspecd-dev wants to merge 5 commits into
cortexkit:mainfrom
unspecd-dev:fix/pi-claude-code-billing

Conversation

@unspecd-dev

@unspecd-dev unspecd-dev commented Aug 11, 2026

Copy link
Copy Markdown

Summary

Two fixes in packages/pi, both reproduced against the released v1.19.0 and verified on a clean macOS VM.

  1. All Claude requests from Pi fail with 400 You're out of extra usage on a valid Claude subscription. Pi's system prompt is pushed into system[] alongside the Claude Code identity block.
  2. claude-opus-5 is not selectable in Pi, despite feat: support Claude Opus 5 #143 adding Opus 5 request conversion and documenting /claude-fast support for it.

1. fix(pi): relocate system prompt out of system[] for Claude Code billing

Symptom

Every Claude request from Pi returns:

400 {"type":"error","error":{"type":"invalid_request_error",
"message":"You're out of extra usage. Add more at claude.ai/settings/usage and keep going."}}

Reproduced on claude-opus-4-8 and claude-sonnet-5. Despite the message, this is not a quota problem: the dumped request is well-formed Claude Code traffic — correct user-agent, the full anthropic-beta set, the stainless headers, and a valid x-anthropic-billing-header carrying cc_version and cch — and the same account works in OpenCode with @cortexkit/opencode-anthropic-auth.

Cause

packages/pi/src/convert.ts pushes Pi's system prompt as a third entry in system[]:

if (context.systemPrompt?.trim()) {
  system.push({ type: 'text', text: sanitize(context.systemPrompt) })
}

Empirically, Anthropic rejects OAuth requests carrying Claude Code billing headers when third-party system content appears in system[] alongside the identity block. Removing that third entry is sufficient to make the request succeed; nothing else about the request changes.

The OpenCode package is unaffected because sanitizeSystemText strips content anchored on OPENCODE_IDENTITY_PREFIX. There is no Pi equivalent, so Pi's prompt passes through untouched.

Released package (1.19.0)status: 400, systemCount: 3, message0Bytes: 30:

[0] x-anthropic-billing-header: cc_version=2.1.177.3bf; cc_entrypoint=cli; cch=0669e;
[1] You are Claude Code, Anthropic's official CLI for Claude.
[2] You are an expert coding assistant operating inside pi, a coding agent harness...

messages[0].content = "hi"

This branchstatus: 200, systemCount: 2, message0Bytes: 3586:

[0] x-anthropic-billing-header: cc_version=2.1.177.3bf; cc_entrypoint=cli; cch=76a11;
[1] You are Claude Code, Anthropic's official CLI for Claude.

messages[0].content = "You are an expert coding assistant operating inside pi... \n\nhi"

Same subscription (account_uuid unchanged), same model, same tool set, same max_tokens and thinking config. The two captures come from different pi sessions and working directories, so the session metadata, the cch value and the prompt text itself differ; the system[] shape is the variable under test.

Fix

Keep only the billing header and identity block in system[]; prepend the sanitized prompt to the first user message, where it is functionally equivalent. When no user message exists, the prompt is dropped rather than pushed back into system[]; that path only occurs when messages[] is already empty.

Two notes for review

Cache anchor. addEphemeralCacheControl anchors on body.system.at(-1), previously Pi's system prompt and now the identity block. Caching still works across turns: a follow-up request in the same session — after a model switch from claude-opus-4-8 to claude-opus-5 — carried an identical message0Hash and message0Bytes. Flagging in case the shorter anchor has implications I have not considered.

Billing-header ordering. buildBillingHeaderValue runs before the relocation, so cch is computed over the first user message as it was before the prompt was prepended. Anthropic accepted the request either way in testing, but you may prefer to compute the header after relocation so the hash matches the transmitted body.


2. fix(pi): register claude-opus-5 in the provider model list

#143 added Opus 5 request conversion in packages/pi/src/convert.ts, wiring up isClaudeOpus5Model and CLAUDE_OPUS_5_ADAPTIVE_THINKING from core, and updated packages/pi/README.md to document /claude-fast support for claude-opus-5. The model was never added to the models array in packages/pi/src/index.ts — that file was last touched by #118.

Without that entry the Opus 5 code path is unreachable from Pi, so the documented model does not appear in /model. The cost entry follows Anthropic's published rates for Opus 5: $5/MTok input, $25/MTok output, $0.50/MTok cache reads and $6.25/MTok cache writes.

A third commit updates packages/pi/README.md, which enumerates the provider catalog and would otherwise omit the newly registered model.


Tests

The relocation changes the shape of messages[0], which packages/pi/src/tests/convert.test.ts asserts on through its shared buildMessages helper. That helper hard-coded systemPrompt: 'test' for every case, including tests concerned only with message conversion, so the relocation broke eight of them. systemPrompt is now an optional parameter: those cases assert raw conversion output as before, and the relocation gets its own coverage — system[] staying at two entries, the string and structured first-user-message paths, the no-user-message path dropping the prompt, and the identity block remaining the last system[] entry that the ephemeral cache anchors on.

packages/pi/src/tests/index.test.ts gains an Opus 5 registration case alongside the existing Sonnet 5 one.

Two notes on layout. The test commit lands after the first two fix commits, so fix(pi): relocate system prompt... is red in isolation; the commits are split that way to keep the two bugs independently revertable. And the root test script is cd packages/opencode && bun run test, so packages/pi's own suite is not reached by CI — it runs with bun run --cwd packages/pi test.


Verification

Clean macOS VM — brew install pi-coding-agent 0.84.1, pi install npm:@cortexkit/pi-anthropic-auth@1.19.0, authenticated with Pi's /login anthropic. The released package reproduced both bugs. This branch was then cloned, built with bun run build, and its packages/pi/dist installed over the released package's dist; core was left at the installed 1.19.0.

bun run typecheck, bun run build, bun run test (1018 pass), bun run --cwd packages/pi test (64 pass), bun run lint and bun run format:check are all clean.

claude-opus-4-8 returns 200 on this branch where the released package returns 400, as dumped above.

claude-opus-5 now appears in /model and returns 200:

model:            claude-opus-5
system[] length:  2
messages[0]       You are an expert coding assistant operating inside pi, a coding agent harness...
thinking          {"type":"adaptive","display":"summarized"}
output_config     {"effort":"medium"}

confirming the CLAUDE_OPUS_5_ADAPTIVE_THINKING path executes. Anthropic validates model IDs — an unrecognised id returns 404 not_found_error — so a successful response confirms the model string was accepted.

Prior art: pankajudhas81/pi-claude-authsrc/transforms.ts documents the same Anthropic behaviour independently and applies the same relocation.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Fixes Claude Code OAuth billing rejections by keeping Pi’s prompt out of system[], and makes claude-opus-5 selectable in Pi’s model list.

  • Bug Fixes
    • Claude billing: keep only the billing header and Claude Code identity in system[]; prepend the sanitized Pi prompt to the first user message, and drop the prompt when no user message exists. Restores successful requests and keeps cache anchoring on the identity block.
    • Model registration: add claude-opus-5 to the provider catalog with reasoning enabled, costs { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 }, contextWindow: 1_000_000, maxTokens: 128_000. Updated packages/pi/README.md.

Written for commit 9c0ec08. Summary will update on new commits.

Review in cubic

Greptile Summary

This PR prevents Claude Code OAuth billing rejection by relocating Pi’s system prompt to the first user message and registers Claude Opus 5 in Pi’s provider catalog.

  • Keeps system[] limited to the billing header and Claude Code identity, including when conversion produces no user message.
  • Prepends the sanitized Pi prompt to string or structured user content.
  • Adds Claude Opus 5 metadata and focused conversion and registration tests.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
packages/pi/src/convert.ts Relocates Pi’s prompt without restoring the rejected fallback shape; the previously reported issue is fixed.
packages/pi/src/index.ts Registers Claude Opus 5 with reasoning, pricing, context-window, and output-token metadata.
packages/pi/src/tests/convert.test.ts Covers prompt relocation for string, structured, absent-user, and absent-prompt request shapes.
packages/pi/src/tests/index.test.ts Verifies the complete Claude Opus 5 provider registration.
packages/pi/README.md Updates the documented Pi model catalog to include Claude Opus 5.

Reviews (2): Last reviewed commit: "fix(pi): drop the system[] fallback for ..." | Re-trigger Greptile

Context used:

Comment thread packages/pi/src/convert.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 5 files

Architecture diagram
sequenceDiagram
    participant UI as "Pi TUI / User"
    participant Ext as "Pi Startup\ncortexKitPiAnthropicAuth"
    participant Catalog as "Provider Model Catalog\n(index.ts)"
    participant Convert as "Request Converter\n(convert.ts)"
    participant Pkg as "@cortexkit/anthropic-auth-core"
    participant API as "Anthropic API"

    Note over UI,API: PR focus: Claude Code billing request shape\nand claude-opus-5 model availability

    rect rgb(240,240,240)
    Note over UI,Catalog: A. Model registration (claude-opus-5)
    UI->>Ext: /model command
    Ext->>Catalog: register provider models
    Catalog->>Catalog: include claude-opus-5 (reasoning,\ncost, context window, max tokens)
    Catalog-->>UI: model appears in /model list
    end

    rect rgb(240,240,240)
    Note over UI,API: B. Request conversion and billing header flow
    UI->>Convert: send user message\n(e.g. "hi")
    Convert->>Pkg: fetch Claude Code billing header\n(x-anthropic-billing-header)
    Pkg-->>Convert: cc_version, cch
    Convert->>Convert: sanitize Pi system prompt\n(no Pi identity fingerprint)
    Convert->>Convert: build system[] array

    Note over Convert: system[] now contains ONLY:\n[0] billing header (added later)\n[1] Claude Code identity block\nPi prompt relocated out of system[]

    Convert->>Convert: find first user message\nin messages[] history

    alt First user message is a plain string
        Convert->>Convert: prepend prompt to string\n-> "You are an expert coding assistant...\n\nhi"
    else First user message is structured content (array)
        Convert->>Convert: unshift text block containing prompt
    else No user message present
        Convert->>Convert: append prompt to system[]\n[NOTE: fallback recreates\nrejected 3-entry shape]
    end

    Convert->>Convert: addEphemeralCacheControl\nanchors on system.at(-1)\n= Claude Code identity block
    Convert->>Convert: buildBillingHeaderValue\ncomputes cch over user message\nBEFORE prompt prepend
    Convert->>API: POST /messages\nwith system[] (2 entries)\n+ user message with prompt

    alt HTTP 200 OK
        API-->>UI: completion (model accepted,\nbilling OK)
    else HTTP 400 (only if fallback path\nrecreates 3-entry system[])
        API-->>Convert: "You're out of extra usage"
    end
    end
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/pi/src/convert.ts Outdated
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