Compare commits
3 Commits
v2.1.0
...
docs/claud
| Author | SHA1 | Date | |
|---|---|---|---|
|
956ca21f54
|
|||
|
b99707c629
|
|||
|
b935209142
|
39
README.md
39
README.md
@@ -52,27 +52,45 @@ const claude = new ClaudeProvider({
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
### Claude Code (subscription via local CLI)
|
### Claude Code (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.
|
||||||
|
|
||||||
**Setup:** install the CLI and run `claude login` once. No API key required.
|
> **Honest status:** the **API-key mode** (Mode B below) works reliably. The **subscription mode** (Mode A) is currently broken on macOS due to an upstream bug in `@anthropic-ai/claude-agent-sdk` — the SDK isolates `CLAUDE_CONFIG_DIR` per invocation and tries to copy a `~/.claude/.credentials.json` file that doesn't exist on macOS (Claude Code stores credentials in the Keychain). The provider misidentifies the OAuth token and bills it as an API key, so subscribers without API credits hit "Credit balance is too low".
|
||||||
|
>
|
||||||
|
> If you have a Pro/Max subscription and need programmatic access today: use the direct [`ClaudeProvider`](#claude-anthropic-api) with an API key, or wait for the upstream SDK fix. The `oauthToken` field is wired up and ready for when Anthropic fixes the Keychain handling.
|
||||||
|
|
||||||
|
**Mode A — Claude Pro/Max subscription** *(see status note above)*:
|
||||||
|
|
||||||
|
```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`
|
||||||
|
|
||||||
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, works reliably):
|
||||||
|
|
||||||
|
```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({
|
||||||
|
apiKey: '...',
|
||||||
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)
|
||||||
@@ -80,10 +98,11 @@ new ClaudeCodeProvider({
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
**Trade-offs to know:**
|
**Trade-offs even when it works:**
|
||||||
- Requires `claude` CLI installed on the host. Not ideal for typical server deployments.
|
- Requires `claude` CLI installed on the host. Not ideal for typical server deployments.
|
||||||
- Higher latency than the direct API (spawns a CLI process per request).
|
- Higher latency than the direct API (spawns a CLI process per request).
|
||||||
- Streaming yields text as the SDK emits successive assistant messages, not token-by-token deltas.
|
- Streaming yields text as the SDK emits successive assistant messages, not token-by-token deltas.
|
||||||
|
- If you have an API key, [`ClaudeProvider`](#claude-anthropic-api) is the simpler, faster choice.
|
||||||
|
|
||||||
### OpenAI
|
### OpenAI
|
||||||
|
|
||||||
|
|||||||
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);
|
||||||
|
});
|
||||||
@@ -2,17 +2,27 @@
|
|||||||
* Claude Code Provider
|
* Claude Code Provider
|
||||||
*
|
*
|
||||||
* Wraps `@anthropic-ai/claude-agent-sdk` so consumers can use a local
|
* Wraps `@anthropic-ai/claude-agent-sdk` so consumers can use a local
|
||||||
* Claude Code installation — including Claude Pro/Max subscription
|
* Claude Code installation through the same BaseAIProvider interface
|
||||||
* accounts authenticated via `claude login` — through the same
|
* as the other providers.
|
||||||
* BaseAIProvider interface as the other providers.
|
|
||||||
*
|
*
|
||||||
* Tradeoffs vs the direct Anthropic API provider:
|
* Status:
|
||||||
* - Authenticates via the local CLI, so subscription users (no API key)
|
* - apiKey mode: works reliably.
|
||||||
* can use it.
|
* - oauthToken (subscription) mode: currently broken on macOS due to an
|
||||||
* - Requires `claude` to be installed and logged in on the host.
|
* upstream SDK bug. The SDK isolates CLAUDE_CONFIG_DIR per invocation
|
||||||
* - Higher latency (shells out to a CLI process per request).
|
* and tries to copy ~/.claude/.credentials.json, which doesn't exist on
|
||||||
|
* macOS (creds live in the Keychain). The OAuth token gets misclassified
|
||||||
|
* as an API key by the SDK, so requests are billed against API credits
|
||||||
|
* instead of the subscription. Subscribers without API credits will see
|
||||||
|
* "Credit balance is too low" errors. The oauthToken field is wired up
|
||||||
|
* and will work as expected once the SDK fix lands upstream.
|
||||||
|
*
|
||||||
|
* Other tradeoffs:
|
||||||
|
* - Requires `claude` CLI installed on the host.
|
||||||
|
* - Higher latency (spawns a CLI process per request).
|
||||||
* - Designed for agent workflows, but used here in single-turn mode
|
* - Designed for agent workflows, but used here in single-turn mode
|
||||||
* (maxTurns: 1, no tools) for plain text completion.
|
* (maxTurns: 1, no tools) for plain text completion.
|
||||||
|
* - If you have an API key, prefer the direct ClaudeProvider for simpler,
|
||||||
|
* lower-latency access.
|
||||||
*
|
*
|
||||||
* @see https://docs.anthropic.com/claude/docs/claude-code-sdk
|
* @see https://docs.anthropic.com/claude/docs/claude-code-sdk
|
||||||
*/
|
*/
|
||||||
@@ -31,12 +41,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 +100,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 +109,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 +126,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 +153,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 +364,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 +372,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 from Claude Code SDK. If you are a Pro/Max subscriber on macOS, this is likely the upstream @anthropic-ai/claude-agent-sdk Keychain bug — the SDK misclassifies the OAuth token as an API key. Workaround: use ClaudeProvider directly with an ANTHROPIC_API_KEY instead.'
|
||||||
},
|
},
|
||||||
rate_limit: {
|
rate_limit: {
|
||||||
type: AIErrorType.RATE_LIMIT,
|
type: AIErrorType.RATE_LIMIT,
|
||||||
|
|||||||
Reference in New Issue
Block a user