# 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 (subscription 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. **Setup:** install the CLI and run `claude login` once. No API key required. ```typescript import { ClaudeCodeProvider } from 'simple-ai-provider'; const claude = new ClaudeCodeProvider({}); // uses local credentials 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: ```typescript new ClaudeCodeProvider({ 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 to know:** - 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. ### 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)