Full rewrite focused on what consumers actually need: install, a working snippet per provider, and the public API surface. Changes vs the previous version: - Document ClaudeCodeProvider, including the subscription-via-CLI auth path that the new provider enables. - Remove the ProviderRegistry section (the class was removed in R1). - Drop the stale Provider Comparison and Detailed Capabilities tables; vendor capabilities and model lists move too fast for a README to track. - Remove the inaccurate Zero Dependencies and Comprehensive Testing claims (post-refactor 44/91 tests need updating). - Refresh default models (Gemini 1.5 -> 2.5) and the package list to match the current build. - Add a brief Architecture note covering the base hooks introduced in R2 and the OpenWebUI strategy split from R3. README is 315 lines (was 701).
316 lines
9.9 KiB
Markdown
316 lines
9.9 KiB
Markdown
# 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<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:
|
|
|
|
```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<T>` 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)
|