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 (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.

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:

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

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%