fix(claude-code): add oauthToken config for subscription SDK auth

`claude login` alone does not authorize SDK / non-interactive
(`claude -p`) usage. Anthropic gates that behind a separate long-lived
token minted by `claude setup-token`. Without it, subscription users
hit `billing_error` from the SDK even though interactive Claude Code
works fine.

Changes:

- ClaudeCodeConfig.oauthToken: new optional field. When set, the
  provider exports it as CLAUDE_CODE_OAUTH_TOKEN before invoking the
  SDK, which makes the SDK bill against the user's Pro/Max
  subscription instead of API credits.
- apiKey continues to work for users with a console API key. If both
  are present, oauthToken takes precedence.
- billing_error / authentication_failed messages now point users at
  `claude setup-token` so the fix is obvious from the error alone.
- README: rewrite the Claude Code section to document both modes and
  the `claude setup-token` step explicitly.
- examples/claude-code.ts: read CLAUDE_CODE_OAUTH_TOKEN from env so
  the smoke test actually works for subscribers.
This commit is contained in:
2026-05-21 14:56:14 +02:00
parent d4871d22fe
commit b935209142
3 changed files with 125 additions and 13 deletions

View File

@@ -31,12 +31,31 @@ import { AIProviderError, AIErrorType } from '../types/index.js';
import { DEFAULT_MODELS } from '../constants.js';
/**
* Configuration for the Claude Code provider. `apiKey` is optional —
* when omitted the SDK falls back to ANTHROPIC_API_KEY in the
* environment or the local subscription credentials managed by the
* `claude` CLI.
* Configuration for the Claude Code provider.
*
* Authentication options, in priority order:
*
* 1. `oauthToken` — long-lived subscription token generated by
* `claude setup-token` (recommended for Pro/Max subscribers using
* the SDK programmatically). Billed against the subscription.
* 2. `apiKey` — standard Anthropic console API key. Billed per-token.
* 3. Environment fallback — the SDK reads `CLAUDE_CODE_OAUTH_TOKEN` or
* `ANTHROPIC_API_KEY` from the environment if neither field is set.
*
* Note: `claude login` alone is not sufficient for SDK use because
* Anthropic gates non-interactive (`claude -p`) invocations behind a
* separate token. Run `claude setup-token` once to mint one.
*/
export interface ClaudeCodeConfig extends Omit<AIProviderConfig, 'apiKey'> {
/**
* Long-lived OAuth token from `claude setup-token`. This is the
* supported path for Claude Pro/Max subscribers to use the SDK
* programmatically — billed against the subscription instead of
* per-token.
*/
oauthToken?: string;
/** Standard Anthropic console API key. Mutually compatible with oauthToken (oauthToken takes precedence). */
apiKey?: string;
/**
@@ -71,6 +90,7 @@ export class ClaudeCodeProvider extends BaseAIProvider {
private readonly cwd: string | undefined;
private readonly allowedTools: string[];
private readonly maxTurns: number;
private readonly oauthToken: string | undefined;
constructor(config: ClaudeCodeConfig) {
super(config as AIProviderConfig);
@@ -79,6 +99,7 @@ export class ClaudeCodeProvider extends BaseAIProvider {
this.cwd = config.cwd;
this.allowedTools = config.allowedTools ?? [];
this.maxTurns = config.maxTurns ?? 1;
this.oauthToken = config.oauthToken;
}
/**
@@ -95,13 +116,21 @@ export class ClaudeCodeProvider extends BaseAIProvider {
);
}
if (config.apiKey !== undefined && (typeof config.apiKey !== 'string')) {
if (config.apiKey !== undefined && typeof config.apiKey !== 'string') {
throw new AIProviderError(
'API key, when provided, must be a string',
AIErrorType.INVALID_REQUEST
);
}
const oauthToken = (config as ClaudeCodeConfig).oauthToken;
if (oauthToken !== undefined && typeof oauthToken !== 'string') {
throw new AIProviderError(
'oauthToken, when provided, must be a string',
AIErrorType.INVALID_REQUEST
);
}
return {
...config,
apiKey: config.apiKey ?? '',
@@ -114,6 +143,9 @@ export class ClaudeCodeProvider extends BaseAIProvider {
// The SDK lazy-spawns the CLI per query; no connection probe is
// necessary (and a probe would cost a real billed turn). We defer
// auth/model errors to the first complete()/stream() call.
if (this.oauthToken) {
process.env.CLAUDE_CODE_OAUTH_TOKEN = this.oauthToken;
}
if (this.config.apiKey) {
process.env.ANTHROPIC_API_KEY = this.config.apiKey;
}
@@ -322,7 +354,7 @@ export class ClaudeCodeProvider extends BaseAIProvider {
const map: Record<string, { type: AIErrorType; message: string }> = {
authentication_failed: {
type: AIErrorType.AUTHENTICATION,
message: 'Claude Code authentication failed. Run `claude login` or set ANTHROPIC_API_KEY.'
message: 'Claude Code authentication failed. For subscriptions run `claude setup-token` and pass the result as `oauthToken`. Otherwise set ANTHROPIC_API_KEY or pass `apiKey`.'
},
oauth_org_not_allowed: {
type: AIErrorType.AUTHENTICATION,
@@ -330,7 +362,7 @@ export class ClaudeCodeProvider extends BaseAIProvider {
},
billing_error: {
type: AIErrorType.AUTHENTICATION,
message: 'Billing error. Check your Anthropic account billing or subscription status.'
message: 'Billing error. For Claude Pro/Max subscribers using the SDK: run `claude setup-token` and pass the resulting token as `oauthToken` (interactive `claude login` alone is not sufficient for non-interactive SDK calls).'
},
rate_limit: {
type: AIErrorType.RATE_LIMIT,