Files
simple-ai-provider/examples/claude-code.ts
Jan-Marlon Leibl 34b2f7314e docs(claude-code): document oauthToken config and the macOS SDK bug
After investigating, the @anthropic-ai/claude-agent-sdk has a macOS-
specific upstream bug that breaks subscription auth: the SDK isolates
CLAUDE_CONFIG_DIR per invocation and tries to copy
~/.claude/.credentials.json, which doesn't exist on macOS (creds live
in the Keychain). The spawned CLI then misclassifies the OAuth token
as an API key and bills against API credits.

Changes:

- ClaudeCodeConfig.oauthToken: new optional config field that exports
  CLAUDE_CODE_OAUTH_TOKEN before invoking the SDK. Wired up and ready
  for when the upstream bug is fixed; currently affected by it on macOS.
- README, JSDoc: clearly document the limitation. API-key mode works
  reliably; subscription mode is broken on macOS pending upstream fix.
- billing_error message: explain the bug and recommend ClaudeProvider +
  ANTHROPIC_API_KEY as the workaround so users can self-diagnose.
- examples/claude-code.ts: read CLAUDE_CODE_OAUTH_TOKEN from env so the
  smoke test attempts the subscription path when configured.

No code changes silently in flight — when the SDK fixes the Keychain
handling, subscribers will Just Work without any code changes here.
2026-05-21 15:10:57 +02:00

62 lines
2.1 KiB
TypeScript

/**
* 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);
});