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
2025-09-04 15:13:09 +02:00
2026-05-21 14:39:36 +02:00
2025-10-01 11:37:20 +00:00

Simple AI Provider

A type-safe TypeScript library with a single API surface for Claude (Anthropic), OpenAI, Google Gemini, OpenWebUI, and Claude Code (subscription-friendly via local CLI).

The same complete() / stream() interface works across every provider, with consistent error types, streaming, and optional structured (typed JSON) output.

Install

npm install simple-ai-provider
# or
bun add simple-ai-provider

Requires Node ≥ 18 (or Bun). TypeScript ≥ 5 is recommended as a peer dependency.

Quick start

import { ClaudeProvider } from 'simple-ai-provider';

const claude = new ClaudeProvider({ apiKey: process.env.ANTHROPIC_API_KEY! });
await claude.initialize();

const response = await claude.complete({
  messages: [
    { role: 'system', content: 'You are a helpful assistant.' },
    { role: 'user', content: 'Explain TypeScript in one sentence.' }
  ],
  maxTokens: 200
});

console.log(response.content);
console.log(response.usage);   // { promptTokens, completionTokens, totalTokens }

Every provider follows the same pattern: construct, initialize(), then complete() or stream().

Providers

Claude (Anthropic API)

import { ClaudeProvider } from 'simple-ai-provider';

const claude = new ClaudeProvider({
  apiKey: process.env.ANTHROPIC_API_KEY!,
  defaultModel: 'claude-3-5-sonnet-20241022', // optional
  version: '2023-06-01',                       // optional
  timeout: 30_000,                             // optional, ms
  maxRetries: 3                                // optional
});

Claude Code (via local CLI)

ClaudeCodeProvider wraps @anthropic-ai/claude-agent-sdk, which spawns the local claude CLI under the hood.

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 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):

# 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
import { ClaudeCodeProvider } from 'simple-ai-provider';

const claude = new ClaudeCodeProvider({
  oauthToken: process.env.CLAUDE_CODE_OAUTH_TOKEN  // from `claude setup-token`
});

Mode B — Console API key (billed per-token, works reliably):

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:

new ClaudeCodeProvider({
  apiKey: '...',
  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)
  cwd: process.cwd()       // working directory for the agent
});

Trade-offs even when it works:

  • 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).
  • Streaming yields text as the SDK emits successive assistant messages, not token-by-token deltas.
  • If you have an API key, ClaudeProvider is the simpler, faster choice.

OpenAI

import { OpenAIProvider } from 'simple-ai-provider';

const openai = new OpenAIProvider({
  apiKey: process.env.OPENAI_API_KEY!,
  defaultModel: 'gpt-4o',
  organization: 'org-...',     // optional
  project: 'proj-...'          // optional
});

baseUrl is supported for OpenAI-compatible endpoints.

Gemini (Google)

import { GeminiProvider } from 'simple-ai-provider';

const gemini = new GeminiProvider({
  apiKey: process.env.GOOGLE_AI_API_KEY!,
  defaultModel: 'gemini-2.5-flash',
  safetySettings: [/* SafetySetting[] from @google/genai */],
  generationConfig: {
    temperature: 0.7,
    topP: 0.8,
    topK: 40,
    maxOutputTokens: 1000
  }
});

Backed by @google/genai (the successor to the deprecated @google/generative-ai).

OpenWebUI (local / self-hosted)

import { OpenWebUIProvider } from 'simple-ai-provider';

const openwebui = new OpenWebUIProvider({
  apiKey: 'your-bearer-token',          // from Settings > Account in OpenWebUI
  baseUrl: 'http://localhost:3000',
  defaultModel: 'llama3.1:latest',
  useOllamaProxy: false,                // false: OpenWebUI chat API (default)
                                        // true:  direct Ollama proxy
  dangerouslyAllowInsecureConnections: true
});

useOllamaProxy flips between two internal strategies — the OpenAI-compatible chat completions endpoint and the direct Ollama generate endpoint.

Streaming

Identical shape across providers:

for await (const chunk of provider.stream({ messages, maxTokens: 200 })) {
  if (!chunk.isComplete) {
    process.stdout.write(chunk.content);
  } else {
    console.log('\n', chunk.usage);
  }
}

Structured output

Ask any provider for a typed JSON response. The library injects a system prompt describing the expected shape and parses the result.

import { createResponseType } from 'simple-ai-provider';

interface UserProfile {
  name: string;
  age: number;
  hobbies: string[];
}

const profileType = createResponseType<UserProfile>(
  'A user profile with name, age, and hobbies',
  { name: 'Alice', age: 30, hobbies: ['climbing', 'photography'] }
);

const response = await claude.complete({
  messages: [{ role: 'user', content: 'Generate a fictional user profile.' }],
  responseType: profileType
});

// response.content is typed as UserProfile
// response.rawContent is the original string
console.log(response.content.name);

This also works with stream() — chunks deliver text, then the final chunk parses.

Factory functions

If you prefer a single entry point:

import { createProvider } from 'simple-ai-provider';

const claude    = createProvider('claude',      { apiKey: '…' });
const openai    = createProvider('openai',      { apiKey: '…' });
const gemini    = createProvider('gemini',      { apiKey: '…' });
const openwebui = createProvider('openwebui',   { apiKey: '…', baseUrl: '…' });
const claudeCode = createProvider('claude-code', {});

Per-provider shortcut factories also exist: createClaudeProvider, createOpenAIProvider, createGeminiProvider, createOpenWebUIProvider, createClaudeCodeProvider.

Error handling

Every error thrown by the library is an AIProviderError with a typed .type and optional .statusCode:

import { AIProviderError, AIErrorType } from 'simple-ai-provider';

try {
  await provider.complete({ messages: [...] });
} catch (error) {
  if (error instanceof AIProviderError) {
    switch (error.type) {
      case AIErrorType.AUTHENTICATION:   /* bad API key, expired token */ break;
      case AIErrorType.RATE_LIMIT:       /* slow down */ break;
      case AIErrorType.MODEL_NOT_FOUND:  /* wrong model name */ break;
      case AIErrorType.INVALID_REQUEST:  /* bad input */ break;
      case AIErrorType.NETWORK:          /* transient connectivity */ break;
      case AIErrorType.TIMEOUT:          /* slow request */ break;
      case AIErrorType.UNKNOWN:          /* fall back */ break;
    }
    console.error(error.statusCode, error.originalError);
  }
}

Configuration reference

All providers share these base options:

Option Type Default Notes
apiKey string required¹ ¹ Optional for ClaudeCodeProvider
baseUrl string provider default Custom or self-hosted endpoint
timeout number 30000 Request timeout in ms
maxRetries number 3 SDK-level retry attempts

Each provider adds its own config (see the sections above).

TypeScript

Full type definitions ship with the package. The main types you'll use:

import type {
  AIMessage,
  CompletionParams,
  CompletionResponse,
  CompletionChunk,
  TokenUsage,
  ProviderInfo,
  ResponseType,

  // Per-provider config interfaces
  ClaudeConfig,
  ClaudeCodeConfig,
  OpenAIConfig,
  GeminiConfig,
  OpenWebUIConfig
} from 'simple-ai-provider';

CompletionResponse<T> is generic — when you pass responseType, content is T (and the original string is preserved on rawContent).

Provider metadata

Every provider exposes getInfo():

const info = provider.getInfo();
//   { name, version, models, maxContextLength, supportsStreaming, capabilities }

Model lists in info.models are example values — refer to each vendor's docs for the current authoritative list. The library accepts any model string the underlying SDK supports.

Architecture (brief)

The library uses a template-method base class (BaseAIProvider) that owns the public lifecycle (initialize, complete, stream), input validation, response-type parsing, and error normalization. Each provider supplies:

  • doInitialize() / doComplete() / doStream() — the actual SDK calls
  • getModelNamePatterns() — regexes for naming-convention warnings
  • sendValidationProbe() — minimal request used during initialize()
  • mapProviderError() / providerErrorMessages() — provider-specific error translation

OpenWebUIProvider additionally uses an internal strategy split (OpenWebUIChatStrategy vs OpenWebUIOllamaStrategy) selected by useOllamaProxy.

Development

git clone https://gitea.jleibl.net/jleibl/simple-ai-provider.git
cd simple-ai-provider
bun install
bun run build

Examples live in examples/:

bun run examples/basic-usage.ts
bun run examples/multi-provider.ts
bun run examples/structured-response-types.ts

Tests in tests/ use Bun's test runner (bun test). Note: post-refactor some tests need updating before they pass cleanly.

License

MIT — see LICENSE.

Description
No description provided
Readme MIT 360 KiB
Languages
TypeScript 100%