Skip to content

feat(anthropic): native Claude Pro/Max subscription support - #349

Merged
shuv1337 merged 1 commit into
integration-v2from
shuvbotta/anthropic-claude-code
Jul 29, 2026
Merged

feat(anthropic): native Claude Pro/Max subscription support#349
shuv1337 merged 1 commit into
integration-v2from
shuvbotta/anthropic-claude-code

Conversation

@shuv1337

Copy link
Copy Markdown
Collaborator

Adds Claude Pro/Max as a first-class Anthropic integration method, so a subscription works without an API key and without an external plugin.

What's included

  • OAuth method claude-pro-max — PKCE, code exchange, refresh.
  • CLAUDE_CODE_OAUTH_TOKEN added to the integration's env method, so a headless host can use claude setup-token. A setup token arrives in the key slot but is an OAuth token — sent as x-api-key it 401s — so isSubscription recognizes the sk-ant-oat prefix and routes it accordingly.
  • No token plumbing added. Integration.connection.resolve already refreshes and persists credentials, and ModelResolver already injects the resolved value.

Why the seam is a route transport

A subscription only draws on the plan when the request presents as genuine Claude Code. Otherwise it is accepted and silently billed as pay-as-you-go "extra usage". That needs Bearer auth, Claude Code's headers, a rewritten request body (identity, canonical <env> block, Claude Code tool casing), and the inverse tool-name mapping on the response.

aisdk.hook("sdk") cannot do this. ModelResolver short-circuits the @ai-sdk/anthropic package to the native AnthropicMessages route, so that hook is never invoked for Anthropic — the existing one in AnthropicPlugin was already dead code on this path. This was confirmed by instrumentation: zero hook invocations across every process while the request still went out and 401'd.

So the seam is a wrapped route transport, selected by a guarded branch in ModelResolver — the same place the ChatGPT-plan branch is selected. Shaping the decoded body leaves encoding with the protocol, and framing splits SSE into whole events before the reverse mapping runs, so it needs no boundary buffering.

The branch is guarded on isSubscription. Without a credential of that shape, behaviour is unchanged.

Billing liability (please read)

normalizeEnv reproduces the <env> block Anthropic fuzzy-matches to decide whether a request is really Claude Code. If opencode's prompt format drifts, requests still succeed and merely cost money — a silent failure. isCanonical is the canary: it warns when normalization stops producing the expected shape, and a golden fixture in the tests pins the format. Please don't silence that warning without re-verifying against a real subscription.

Verification

Against a live Claude Pro/Max subscription, on a from-source server with an isolated XDG_CONFIG_HOME so no external plugin could load:

  • models catalogued; completion returned from claude-sonnet-5
  • tool turn round-tripped: opencode readRead on the wire → read restored from response frames → correct answer
  • CLAUDE_CODE_OAUTH_TOKEN picked up natively as an env connection
  • 27 new tests; 1485 pass / 0 fail across packages/core; typecheck clean; oxlint 0 errors on touched files

Adds Claude Pro/Max as a first-class Anthropic integration method, so a
subscription can be used without an API key and without an external plugin.

Auth is an OAuth method (`claude-pro-max`, PKCE + code exchange + refresh),
plus CLAUDE_CODE_OAUTH_TOKEN added to the integration's env method so a
headless host can use `claude setup-token`. A setup token arrives in the key
slot but is an OAuth token -- sent as x-api-key it 401s -- so isSubscription
recognizes the sk-ant-oat prefix and routes it accordingly.

No token plumbing is added: Integration.connection.resolve already refreshes
and persists credentials, and ModelResolver already injects the resolved value.

Why the seam is a route transport
---------------------------------
A subscription only draws on the plan when the request presents as genuine
Claude Code; otherwise it is silently billed as pay-as-you-go "extra usage".
That requires Bearer auth, Claude Code's headers, a rewritten request body
(Claude Code identity, canonical <env> block, Claude Code tool casing) and the
inverse tool-name mapping on the response.

`aisdk.hook("sdk")` cannot do this: ModelResolver short-circuits the
`@ai-sdk/anthropic` package to the native AnthropicMessages route, so that hook
is never invoked for Anthropic (the existing one in AnthropicPlugin was already
dead code on this path). The seam is therefore a wrapped route transport,
selected by a guarded branch in ModelResolver -- the same place the ChatGPT-plan
branch is selected. Shaping the decoded body leaves encoding with the protocol,
and framing splits SSE into whole events before the reverse mapping runs, so it
needs no boundary buffering.

The branch is guarded on isSubscription: without a credential of that shape,
behaviour is unchanged.

Billing liability
-----------------
normalizeEnv reproduces the <env> block Anthropic fuzzy-matches to decide
whether a request is really Claude Code. If opencode's prompt format drifts,
the request still succeeds and just costs money -- a silent failure. isCanonical
is the canary: it warns when normalization no longer produces the expected
shape, and a golden fixture in the tests pins the format.

Verified against a live Claude Pro/Max subscription: models are catalogued, a
completion returns, and a tool-using turn round-trips (opencode `read` ->
Claude Code `Read` on the wire -> `read` restored from the response frames).
@blacksmith-sh

blacksmith-sh Bot commented Jul 29, 2026

Copy link
Copy Markdown

Found 1 test failure on Blacksmith runners:

Failure

Test View Logs
smoke: session timeline/renders seeded timeline in order while paging through history View Logs

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

@shuv1337
shuv1337 merged commit ea19eba into integration-v2 Jul 29, 2026
3 of 4 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