feat(anthropic): native Claude Pro/Max subscription support - #349
Merged
Conversation
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).
|
Found 1 test failure on Blacksmith runners: Failure
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
![Fix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/fix-with-codesmith-light.png)
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
claude-pro-max— PKCE, code exchange, refresh.CLAUDE_CODE_OAUTH_TOKENadded to the integration'senvmethod, so a headless host can useclaude setup-token. A setup token arrives in the key slot but is an OAuth token — sent asx-api-keyit 401s — soisSubscriptionrecognizes thesk-ant-oatprefix and routes it accordingly.Integration.connection.resolvealready refreshes and persists credentials, andModelResolveralready 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.ModelResolvershort-circuits the@ai-sdk/anthropicpackage to the nativeAnthropicMessagesroute, so that hook is never invoked for Anthropic — the existing one inAnthropicPluginwas 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)
normalizeEnvreproduces 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.isCanonicalis 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_HOMEso no external plugin could load:claude-sonnet-5read→Readon the wire →readrestored from response frames → correct answerCLAUDE_CODE_OAUTH_TOKENpicked up natively as anenvconnection1485 pass / 0 failacrosspackages/core; typecheck clean; oxlint 0 errors on touched files