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