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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user