# 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 ```bash 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 ```typescript 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) ```typescript 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`](#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 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): ```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({ 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`](#claude-anthropic-api) is the simpler, faster choice. ### OpenAI ```typescript 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) ```typescript 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) ```typescript 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: ```typescript 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. ```typescript import { createResponseType } from 'simple-ai-provider'; interface UserProfile { name: string; age: number; hobbies: string[]; } const profileType = createResponseType( '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: ```typescript 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`: ```typescript 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: ```typescript import type { AIMessage, CompletionParams, CompletionResponse, CompletionChunk, TokenUsage, ProviderInfo, ResponseType, // Per-provider config interfaces ClaudeConfig, ClaudeCodeConfig, OpenAIConfig, GeminiConfig, OpenWebUIConfig } from 'simple-ai-provider'; ``` `CompletionResponse` is generic — when you pass `responseType`, `content` is `T` (and the original string is preserved on `rawContent`). ## Provider metadata Every provider exposes `getInfo()`: ```typescript 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 ```bash git clone https://gitea.jleibl.net/jleibl/simple-ai-provider.git cd simple-ai-provider bun install bun run build ``` Examples live in `examples/`: ```bash 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](LICENSE). ## Links - [Anthropic Claude API](https://docs.anthropic.com/claude/reference/) - [Claude Code SDK](https://docs.anthropic.com/claude/docs/claude-code-sdk) - [OpenAI API](https://platform.openai.com/docs/) - [Google Gen AI SDK](https://ai.google.dev/) - [OpenWebUI](https://openwebui.com/) - [Repository](https://gitea.jleibl.net/jleibl/simple-ai-provider)