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
README.md
31
README.md
@@ -52,27 +52,46 @@ const claude = new ClaudeProvider({
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
### Claude Code (subscription via local CLI)
|
### Claude Code (subscription or API key via local CLI)
|
||||||
|
|
||||||
`ClaudeCodeProvider` wraps `@anthropic-ai/claude-agent-sdk`, which authenticates through the local `claude` CLI. This is the supported path for **Claude Pro / Max subscribers** who don't have a console API key.
|
`ClaudeCodeProvider` wraps `@anthropic-ai/claude-agent-sdk`, which spawns the local `claude` CLI under the hood. It supports two billing modes:
|
||||||
|
|
||||||
**Setup:** install the CLI and run `claude login` once. No API key required.
|
**Mode A — Claude Pro/Max subscription** (billed against subscription credits):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# One-time: install the CLI, then mint a long-lived OAuth token.
|
||||||
|
# `claude login` alone is NOT enough — Anthropic gates SDK use behind setup-token.
|
||||||
|
claude setup-token
|
||||||
|
```
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { ClaudeCodeProvider } from 'simple-ai-provider';
|
import { ClaudeCodeProvider } from 'simple-ai-provider';
|
||||||
|
|
||||||
const claude = new ClaudeCodeProvider({}); // uses local credentials
|
const claude = new ClaudeCodeProvider({
|
||||||
await claude.initialize();
|
oauthToken: process.env.CLAUDE_CODE_OAUTH_TOKEN // from `claude setup-token`
|
||||||
|
});
|
||||||
|
|
||||||
|
await claude.initialize();
|
||||||
const response = await claude.complete({
|
const response = await claude.complete({
|
||||||
messages: [{ role: 'user', content: 'Hello!' }]
|
messages: [{ role: 'user', content: 'Hello!' }]
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
You can still pass `apiKey` to override (it's set as `ANTHROPIC_API_KEY` for the SDK). Optional config:
|
**Mode B — Console API key** (billed per-token):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const claude = new ClaudeCodeProvider({
|
||||||
|
apiKey: process.env.ANTHROPIC_API_KEY
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
If both are present, `oauthToken` wins. Either field can also be picked up from the environment (`CLAUDE_CODE_OAUTH_TOKEN` / `ANTHROPIC_API_KEY`) without being passed explicitly.
|
||||||
|
|
||||||
|
Optional config:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
new ClaudeCodeProvider({
|
new ClaudeCodeProvider({
|
||||||
|
oauthToken: '...',
|
||||||
defaultModel: 'sonnet', // 'sonnet' | 'opus' | 'haiku' | 'inherit' | full model ID
|
defaultModel: 'sonnet', // 'sonnet' | 'opus' | 'haiku' | 'inherit' | full model ID
|
||||||
maxTurns: 1, // 1 for plain completion; raise for agent/tool loops
|
maxTurns: 1, // 1 for plain completion; raise for agent/tool loops
|
||||||
allowedTools: [], // tool names to enable (default: none)
|
allowedTools: [], // tool names to enable (default: none)
|
||||||
|
|||||||
61
examples/claude-code.ts
Normal file
61
examples/claude-code.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* Quick smoke test for ClaudeCodeProvider.
|
||||||
|
*
|
||||||
|
* Prereqs (pick one):
|
||||||
|
* - Claude Pro/Max subscription: run `claude setup-token`, then set
|
||||||
|
* CLAUDE_CODE_OAUTH_TOKEN=<the token it prints> in your environment
|
||||||
|
* (or pass it as `oauthToken` below).
|
||||||
|
* - Console API key: set ANTHROPIC_API_KEY (or pass `apiKey`).
|
||||||
|
*
|
||||||
|
* `claude login` alone is NOT sufficient for SDK use — Anthropic gates
|
||||||
|
* non-interactive (`claude -p`) invocations behind a separate token.
|
||||||
|
*
|
||||||
|
* Run: bun run examples/claude-code.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ClaudeCodeProvider } from '../src/index.js';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const claude = new ClaudeCodeProvider({
|
||||||
|
defaultModel: 'sonnet',
|
||||||
|
oauthToken: process.env.CLAUDE_CODE_OAUTH_TOKEN
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Initializing…');
|
||||||
|
await claude.initialize();
|
||||||
|
console.log('Ready.\n');
|
||||||
|
|
||||||
|
// ── Non-streaming completion ───────────────────────────────────────────
|
||||||
|
console.log('--- complete() ---');
|
||||||
|
const response = await claude.complete({
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: 'You are concise. Reply in one sentence.' },
|
||||||
|
{ role: 'user', content: 'What is the capital of Japan?' }
|
||||||
|
],
|
||||||
|
maxTokens: 100
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(response.content);
|
||||||
|
console.log(`tokens: ${response.usage.totalTokens} | cost: $${response.metadata?.costUsd ?? '?'}\n`);
|
||||||
|
|
||||||
|
// ── Streaming ──────────────────────────────────────────────────────────
|
||||||
|
console.log('--- stream() ---');
|
||||||
|
let totalTokens = 0;
|
||||||
|
for await (const chunk of claude.stream({
|
||||||
|
messages: [{ role: 'user', content: 'Count from 1 to 5, one per line.' }],
|
||||||
|
maxTokens: 100
|
||||||
|
})) {
|
||||||
|
if (!chunk.isComplete) {
|
||||||
|
process.stdout.write(chunk.content);
|
||||||
|
} else {
|
||||||
|
totalTokens = chunk.usage?.totalTokens ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(`\ntokens: ${totalTokens}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error('Failed:', err.message);
|
||||||
|
if (err.type) console.error('Type:', err.type);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -31,12 +31,31 @@ import { AIProviderError, AIErrorType } from '../types/index.js';
|
|||||||
import { DEFAULT_MODELS } from '../constants.js';
|
import { DEFAULT_MODELS } from '../constants.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration for the Claude Code provider. `apiKey` is optional —
|
* Configuration for the Claude Code provider.
|
||||||
* when omitted the SDK falls back to ANTHROPIC_API_KEY in the
|
*
|
||||||
* environment or the local subscription credentials managed by the
|
* Authentication options, in priority order:
|
||||||
* `claude` CLI.
|
*
|
||||||
|
* 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'> {
|
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;
|
apiKey?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,6 +90,7 @@ export class ClaudeCodeProvider extends BaseAIProvider {
|
|||||||
private readonly cwd: string | undefined;
|
private readonly cwd: string | undefined;
|
||||||
private readonly allowedTools: string[];
|
private readonly allowedTools: string[];
|
||||||
private readonly maxTurns: number;
|
private readonly maxTurns: number;
|
||||||
|
private readonly oauthToken: string | undefined;
|
||||||
|
|
||||||
constructor(config: ClaudeCodeConfig) {
|
constructor(config: ClaudeCodeConfig) {
|
||||||
super(config as AIProviderConfig);
|
super(config as AIProviderConfig);
|
||||||
@@ -79,6 +99,7 @@ export class ClaudeCodeProvider extends BaseAIProvider {
|
|||||||
this.cwd = config.cwd;
|
this.cwd = config.cwd;
|
||||||
this.allowedTools = config.allowedTools ?? [];
|
this.allowedTools = config.allowedTools ?? [];
|
||||||
this.maxTurns = config.maxTurns ?? 1;
|
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(
|
throw new AIProviderError(
|
||||||
'API key, when provided, must be a string',
|
'API key, when provided, must be a string',
|
||||||
AIErrorType.INVALID_REQUEST
|
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 {
|
return {
|
||||||
...config,
|
...config,
|
||||||
apiKey: config.apiKey ?? '',
|
apiKey: config.apiKey ?? '',
|
||||||
@@ -114,6 +143,9 @@ export class ClaudeCodeProvider extends BaseAIProvider {
|
|||||||
// The SDK lazy-spawns the CLI per query; no connection probe is
|
// The SDK lazy-spawns the CLI per query; no connection probe is
|
||||||
// necessary (and a probe would cost a real billed turn). We defer
|
// necessary (and a probe would cost a real billed turn). We defer
|
||||||
// auth/model errors to the first complete()/stream() call.
|
// auth/model errors to the first complete()/stream() call.
|
||||||
|
if (this.oauthToken) {
|
||||||
|
process.env.CLAUDE_CODE_OAUTH_TOKEN = this.oauthToken;
|
||||||
|
}
|
||||||
if (this.config.apiKey) {
|
if (this.config.apiKey) {
|
||||||
process.env.ANTHROPIC_API_KEY = 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 }> = {
|
const map: Record<string, { type: AIErrorType; message: string }> = {
|
||||||
authentication_failed: {
|
authentication_failed: {
|
||||||
type: AIErrorType.AUTHENTICATION,
|
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: {
|
oauth_org_not_allowed: {
|
||||||
type: AIErrorType.AUTHENTICATION,
|
type: AIErrorType.AUTHENTICATION,
|
||||||
@@ -330,7 +362,7 @@ export class ClaudeCodeProvider extends BaseAIProvider {
|
|||||||
},
|
},
|
||||||
billing_error: {
|
billing_error: {
|
||||||
type: AIErrorType.AUTHENTICATION,
|
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: {
|
rate_limit: {
|
||||||
type: AIErrorType.RATE_LIMIT,
|
type: AIErrorType.RATE_LIMIT,
|
||||||
|
|||||||
Reference in New Issue
Block a user