Merge pull request 'fix(claude-code): add oauthToken config for subscription SDK auth' (#12) from fix/claude-code-oauth-token into main
Reviewed-on: #12
This commit was merged in pull request #12.
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
|
||||
import { ClaudeCodeProvider } from 'simple-ai-provider';
|
||||
|
||||
const claude = new ClaudeCodeProvider({}); // uses local credentials
|
||||
await claude.initialize();
|
||||
const claude = new ClaudeCodeProvider({
|
||||
oauthToken: process.env.CLAUDE_CODE_OAUTH_TOKEN // from `claude setup-token`
|
||||
});
|
||||
|
||||
await claude.initialize();
|
||||
const response = await claude.complete({
|
||||
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
|
||||
new ClaudeCodeProvider({
|
||||
oauthToken: '...',
|
||||
defaultModel: 'sonnet', // 'sonnet' | 'opus' | 'haiku' | 'inherit' | full model ID
|
||||
maxTurns: 1, // 1 for plain completion; raise for agent/tool loops
|
||||
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';
|
||||
|
||||
/**
|
||||
* 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