feat: add initial implementation of Simple AI Provider package

This commit is contained in:
2025-05-28 11:54:24 +02:00
commit 42902445fb
15 changed files with 1616 additions and 0 deletions

34
.gitignore vendored Normal file
View File

@ -0,0 +1,34 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

32
.npmignore Normal file
View File

@ -0,0 +1,32 @@
# Source files
src/
examples/
*.test.ts
*.spec.ts
# Development files
.gitignore
tsconfig.json
bun.lock
# Build artifacts (keep only dist)
node_modules/
# Documentation (keep README.md)
docs/
# IDE files
.vscode/
.idea/
*.swp
*.swo
# OS files
.DS_Store
Thumbs.db
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

284
README.md Normal file
View File

@ -0,0 +1,284 @@
# Simple AI Provider
A professional, extensible TypeScript package for integrating multiple AI providers into your applications with a unified interface. Currently supports Claude (Anthropic) with plans to add more providers.
## Features
- 🎯 **Unified Interface**: Same API across all AI providers
- 🔒 **Type-Safe**: Full TypeScript support with comprehensive type definitions
- 🚀 **Easy to Use**: Simple factory functions and intuitive configuration
- 📡 **Streaming Support**: Real-time streaming responses where supported
- 🛡️ **Error Handling**: Robust error handling with categorized error types
- 🔧 **Extensible**: Easy to add new AI providers
- 📦 **Modern**: Built with ES modules and modern JavaScript features
## Installation
```bash
npm install simple-ai-provider
# or
yarn add simple-ai-provider
# or
bun add simple-ai-provider
```
## Quick Start
### Basic Usage with Claude
```typescript
import { createClaudeProvider } from 'simple-ai-provider';
// Create a Claude provider
const claude = createClaudeProvider('your-anthropic-api-key');
// Initialize the provider
await claude.initialize();
// Generate a completion
const response = await claude.complete({
messages: [
{ role: 'user', content: 'Hello! How are you today?' }
],
maxTokens: 100,
temperature: 0.7
});
console.log(response.content);
```
### Streaming Responses
```typescript
import { createClaudeProvider } from 'simple-ai-provider';
const claude = createClaudeProvider('your-anthropic-api-key');
await claude.initialize();
// Stream a completion
for await (const chunk of claude.stream({
messages: [
{ role: 'user', content: 'Write a short story about a robot.' }
],
maxTokens: 500
})) {
if (!chunk.isComplete) {
process.stdout.write(chunk.content);
} else {
console.log('\n\nUsage:', chunk.usage);
}
}
```
### Advanced Configuration
```typescript
import { ClaudeProvider } from 'simple-ai-provider';
const claude = new ClaudeProvider({
apiKey: 'your-anthropic-api-key',
defaultModel: 'claude-3-5-sonnet-20241022',
timeout: 30000,
maxRetries: 3,
baseUrl: 'https://api.anthropic.com' // optional custom endpoint
});
await claude.initialize();
const response = await claude.complete({
messages: [
{ role: 'system', content: 'You are a helpful assistant.' },
{ role: 'user', content: 'Explain quantum computing in simple terms.' }
],
model: 'claude-3-5-haiku-20241022',
maxTokens: 300,
temperature: 0.5,
topP: 0.9,
stopSequences: ['\n\n']
});
```
## API Reference
### Core Types
#### `AIMessage`
```typescript
interface AIMessage {
role: 'system' | 'user' | 'assistant';
content: string;
metadata?: Record<string, any>;
}
```
#### `CompletionParams`
```typescript
interface CompletionParams {
messages: AIMessage[];
model?: string;
maxTokens?: number;
temperature?: number;
topP?: number;
stopSequences?: string[];
stream?: boolean;
}
```
#### `CompletionResponse`
```typescript
interface CompletionResponse {
content: string;
model: string;
usage: TokenUsage;
id: string;
metadata?: Record<string, any>;
}
```
### Factory Functions
#### `createClaudeProvider(apiKey, options?)`
Creates a Claude provider with simplified configuration.
```typescript
const claude = createClaudeProvider('your-api-key', {
defaultModel: 'claude-3-5-sonnet-20241022',
timeout: 30000
});
```
#### `createProvider(type, config)`
Generic factory function for creating any provider type.
```typescript
const claude = createProvider('claude', {
apiKey: 'your-api-key',
defaultModel: 'claude-3-5-sonnet-20241022'
});
```
### Provider Methods
#### `initialize(): Promise<void>`
Initializes the provider and validates the configuration.
#### `complete(params): Promise<CompletionResponse>`
Generates a completion based on the provided parameters.
#### `stream(params): AsyncIterable<CompletionChunk>`
Generates a streaming completion.
#### `getInfo(): ProviderInfo`
Returns information about the provider and its capabilities.
#### `isInitialized(): boolean`
Checks if the provider has been initialized.
## Error Handling
The package provides comprehensive error handling with categorized error types:
```typescript
import { AIProviderError, AIErrorType } from 'simple-ai-provider';
try {
const response = await claude.complete({
messages: [{ role: 'user', content: 'Hello!' }]
});
} catch (error) {
if (error instanceof AIProviderError) {
switch (error.type) {
case AIErrorType.AUTHENTICATION:
console.error('Invalid API key');
break;
case AIErrorType.RATE_LIMIT:
console.error('Rate limit exceeded');
break;
case AIErrorType.INVALID_REQUEST:
console.error('Invalid request parameters');
break;
default:
console.error('Unknown error:', error.message);
}
}
}
```
## Supported Models
### Claude (Anthropic)
- `claude-3-5-sonnet-20241022` (default)
- `claude-3-5-haiku-20241022`
- `claude-3-opus-20240229`
- `claude-3-sonnet-20240229`
- `claude-3-haiku-20240307`
## Environment Variables
You can set your API keys as environment variables:
```bash
export ANTHROPIC_API_KEY="your-anthropic-api-key"
```
```typescript
const claude = createClaudeProvider(process.env.ANTHROPIC_API_KEY!);
```
## Best Practices
1. **Always initialize providers** before using them
2. **Handle errors gracefully** with proper error types
3. **Use appropriate models** for your use case
4. **Set reasonable timeouts** for your application
5. **Implement retry logic** for production applications
6. **Monitor token usage** to control costs
## Extending the Package
To add a new AI provider, extend the `BaseAIProvider` class:
```typescript
import { BaseAIProvider } from 'simple-ai-provider';
class MyCustomProvider extends BaseAIProvider {
protected async doInitialize(): Promise<void> {
// Initialize your provider
}
protected async doComplete(params: CompletionParams): Promise<CompletionResponse> {
// Implement completion logic
}
protected async *doStream(params: CompletionParams): AsyncIterable<CompletionChunk> {
// Implement streaming logic
}
public getInfo(): ProviderInfo {
return {
name: 'MyCustomProvider',
version: '1.0.0',
models: ['my-model'],
maxContextLength: 4096,
supportsStreaming: true
};
}
}
```
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
## License
MIT
## Changelog
### 1.0.0
- Initial release
- Claude provider implementation
- Streaming support
- Comprehensive error handling
- TypeScript support

35
bun.lock Normal file
View File

@ -0,0 +1,35 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "simple-ai-provider",
"dependencies": {
"@anthropic-ai/sdk": "^0.52.0",
},
"devDependencies": {
"@types/bun": "latest",
"@types/node": "^20.0.0",
},
"peerDependencies": {
"typescript": "^5",
},
},
},
"packages": {
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.52.0", "", { "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-d4c+fg+xy9e46c8+YnrrgIQR45CZlAi7PwdzIfDXDM6ACxEZli1/fxhURsq30ZpMZy6LvSkr41jGq5aF5TD7rQ=="],
"@types/bun": ["@types/bun@1.2.14", "", { "dependencies": { "bun-types": "1.2.14" } }, "sha512-VsFZKs8oKHzI7zwvECiAJ5oSorWndIWEVhfbYqZd4HI/45kzW7PN2Rr5biAzvGvRuNmYLSANY+H59ubHq8xw7Q=="],
"@types/node": ["@types/node@20.17.51", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-hccptBl7C8lHiKxTBsY6vYYmqpmw1E/aGR/8fmueE+B390L3pdMOpNSRvFO4ZnXzW5+p2HBXV0yNABd2vdk22Q=="],
"bun-types": ["bun-types@1.2.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-Kuh4Ub28ucMRWeiUUWMHsT9Wcbr4H3kLIO72RZZElSDxSu7vpetRvxIUDUaW6QtaIeixIpm7OXtNnZPf82EzwA=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="],
"bun-types/@types/node": ["@types/node@22.15.23", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-7Ec1zaFPF4RJ0eXu1YT/xgiebqwqoJz8rYPDi/O2BcZ++Wpt0Kq9cl0eg6NN6bYbPnR67ZLo7St5Q3UK0SnARw=="],
"bun-types/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
}
}

82
examples/basic-usage.ts Normal file
View File

@ -0,0 +1,82 @@
/**
* Basic usage example for Simple AI Provider
* Demonstrates how to use the Claude provider
*/
import { createClaudeProvider, AIProviderError, AIErrorType } from '../src/index.js';
async function basicExample() {
// Replace with your actual API key
const apiKey = process.env.ANTHROPIC_API_KEY || 'your-api-key-here';
try {
// Create and initialize the Claude provider
console.log('Creating Claude provider...');
const claude = createClaudeProvider(apiKey, {
defaultModel: 'claude-3-5-haiku-20241022', // Using faster model for demo
timeout: 30000
});
console.log('Initializing provider...');
await claude.initialize();
console.log('Provider info:', claude.getInfo());
// Basic completion
console.log('\n--- Basic Completion ---');
const response = await claude.complete({
messages: [
{ role: 'system', content: 'You are a helpful assistant that responds concisely.' },
{ role: 'user', content: 'What is TypeScript?' }
],
maxTokens: 150,
temperature: 0.7
});
console.log('Response:', response.content);
console.log('Usage:', response.usage);
// Streaming example
console.log('\n--- Streaming Example ---');
console.log('Streaming response for: "Write a haiku about coding"\n');
for await (const chunk of claude.stream({
messages: [
{ role: 'user', content: 'Write a haiku about coding' }
],
maxTokens: 100
})) {
if (!chunk.isComplete) {
process.stdout.write(chunk.content);
} else {
console.log('\n\nStream completed. Usage:', chunk.usage);
}
}
} catch (error) {
if (error instanceof AIProviderError) {
console.error(`AI Provider Error (${error.type}):`, error.message);
switch (error.type) {
case AIErrorType.AUTHENTICATION:
console.error('Please check your API key.');
break;
case AIErrorType.RATE_LIMIT:
console.error('You are being rate limited. Please wait and try again.');
break;
case AIErrorType.INVALID_REQUEST:
console.error('Invalid request parameters.');
break;
default:
console.error('An unexpected error occurred.');
}
} else {
console.error('Unexpected error:', error);
}
}
}
// Run the example
if (import.meta.main) {
basicExample().catch(console.error);
}

57
package.json Normal file
View File

@ -0,0 +1,57 @@
{
"name": "simple-ai-provider",
"version": "1.0.0",
"description": "A simple and extensible AI provider package for easy integration of multiple AI services",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"files": [
"dist",
"README.md",
"package.json"
],
"scripts": {
"build": "bun run build:clean && bun run build:types && bun run build:esm && bun run build:cjs",
"build:clean": "rm -rf dist",
"build:types": "tsc --project tsconfig.build.json",
"build:esm": "bun build src/index.ts --outfile dist/index.mjs --format esm",
"build:cjs": "bun build src/index.ts --outfile dist/index.js --format cjs",
"dev": "bun run src/index.ts",
"test": "bun test",
"prepublishOnly": "bun run build"
},
"keywords": [
"ai",
"claude",
"anthropic",
"provider",
"typescript",
"nodejs"
],
"author": "Jan-Marlon Leibl",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://gitea.jleibl.net/jleibl/simple-ai-provider.git"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.52.0"
},
"devDependencies": {
"@types/bun": "latest",
"@types/node": "^20.0.0"
},
"peerDependencies": {
"typescript": "^5"
},
"engines": {
"node": ">=18.0.0"
}
}

52
src/index.ts Normal file
View File

@ -0,0 +1,52 @@
/**
* Simple AI Provider - Main Entry Point
*
* A professional, extensible package for integrating multiple AI providers
* into your applications with a unified interface.
*
* @author Jan-Marlon Leibl
* @version 1.0.0
*/
// Core types and interfaces
export type {
AIProviderConfig,
AIMessage,
MessageRole,
CompletionParams,
CompletionResponse,
CompletionChunk,
TokenUsage,
ProviderInfo
} from './types/index.js';
// Error handling
export { AIProviderError, AIErrorType } from './types/index.js';
// Base provider class
export { BaseAIProvider } from './providers/base.js';
// Concrete provider implementations
export { ClaudeProvider, type ClaudeConfig } from './providers/claude.js';
// Utility functions and factory
export {
createProvider,
createClaudeProvider,
ProviderRegistry,
type ProviderType,
type ProviderConfigMap
} from './utils/factory.js';
// Re-export everything from providers for convenience
export * from './providers/index.js';
/**
* Package version
*/
export const VERSION = '1.0.0';
/**
* List of supported providers
*/
export const SUPPORTED_PROVIDERS = ['claude'] as const;

264
src/providers/base.ts Normal file
View File

@ -0,0 +1,264 @@
/**
* Abstract base class for all AI providers
* Provides common functionality and enforces a consistent interface
*/
import type {
AIProviderConfig,
CompletionParams,
CompletionResponse,
CompletionChunk,
ProviderInfo
} from '../types/index.js';
import { AIProviderError, AIErrorType } from '../types/index.js';
/**
* Abstract base class that all AI providers must extend
*/
export abstract class BaseAIProvider {
protected config: AIProviderConfig;
protected initialized: boolean = false;
constructor(config: AIProviderConfig) {
this.config = this.validateConfig(config);
}
/**
* Validates the provider configuration
* @param config - Configuration object to validate
* @returns Validated configuration
* @throws AIProviderError if configuration is invalid
*/
protected validateConfig(config: AIProviderConfig): AIProviderConfig {
if (!config.apiKey || typeof config.apiKey !== 'string') {
throw new AIProviderError(
'API key is required and must be a string',
AIErrorType.INVALID_REQUEST
);
}
// Set default values
const validatedConfig = {
...config,
timeout: config.timeout ?? 30000,
maxRetries: config.maxRetries ?? 3
};
return validatedConfig;
}
/**
* Initialize the provider (setup connections, validate credentials, etc.)
* Must be called before using the provider
*/
public async initialize(): Promise<void> {
try {
await this.doInitialize();
this.initialized = true;
} catch (error) {
throw this.handleError(error as Error);
}
}
/**
* Check if the provider is initialized
*/
public isInitialized(): boolean {
return this.initialized;
}
/**
* Generate a completion based on the provided parameters
* @param params - Parameters for the completion request
* @returns Promise resolving to completion response
*/
public async complete(params: CompletionParams): Promise<CompletionResponse> {
this.ensureInitialized();
this.validateCompletionParams(params);
try {
return await this.doComplete(params);
} catch (error) {
throw this.handleError(error as Error);
}
}
/**
* Generate a streaming completion
* @param params - Parameters for the completion request
* @returns AsyncIterable of completion chunks
*/
public async *stream(params: CompletionParams): AsyncIterable<CompletionChunk> {
this.ensureInitialized();
this.validateCompletionParams(params);
try {
yield* this.doStream(params);
} catch (error) {
throw this.handleError(error as Error);
}
}
/**
* Get information about this provider
* @returns Provider information and capabilities
*/
public abstract getInfo(): ProviderInfo;
/**
* Provider-specific initialization logic
* Override this method in concrete implementations
*/
protected abstract doInitialize(): Promise<void>;
/**
* Provider-specific completion logic
* Override this method in concrete implementations
*/
protected abstract doComplete(params: CompletionParams): Promise<CompletionResponse>;
/**
* Provider-specific streaming logic
* Override this method in concrete implementations
*/
protected abstract doStream(params: CompletionParams): AsyncIterable<CompletionChunk>;
/**
* Ensures the provider is initialized before use
* @throws AIProviderError if not initialized
*/
protected ensureInitialized(): void {
if (!this.initialized) {
throw new AIProviderError(
'Provider must be initialized before use. Call initialize() first.',
AIErrorType.INVALID_REQUEST
);
}
}
/**
* Validates completion parameters
* @param params - Parameters to validate
* @throws AIProviderError if parameters are invalid
*/
protected validateCompletionParams(params: CompletionParams): void {
if (!params.messages || !Array.isArray(params.messages) || params.messages.length === 0) {
throw new AIProviderError(
'Messages array is required and must not be empty',
AIErrorType.INVALID_REQUEST
);
}
for (const message of params.messages) {
if (!message.role || !['system', 'user', 'assistant'].includes(message.role)) {
throw new AIProviderError(
'Each message must have a valid role (system, user, or assistant)',
AIErrorType.INVALID_REQUEST
);
}
if (!message.content || typeof message.content !== 'string') {
throw new AIProviderError(
'Each message must have non-empty string content',
AIErrorType.INVALID_REQUEST
);
}
}
if (params.temperature !== undefined && (params.temperature < 0 || params.temperature > 1)) {
throw new AIProviderError(
'Temperature must be between 0.0 and 1.0',
AIErrorType.INVALID_REQUEST
);
}
if (params.topP !== undefined && (params.topP < 0 || params.topP > 1)) {
throw new AIProviderError(
'Top-p must be between 0.0 and 1.0',
AIErrorType.INVALID_REQUEST
);
}
if (params.maxTokens !== undefined && params.maxTokens < 1) {
throw new AIProviderError(
'Max tokens must be a positive integer',
AIErrorType.INVALID_REQUEST
);
}
}
/**
* Handles and transforms errors into AIProviderError instances
* @param error - The original error
* @returns AIProviderError with appropriate type and context
*/
protected handleError(error: Error): AIProviderError {
if (error instanceof AIProviderError) {
return error;
}
// Handle common HTTP status codes
if ('status' in error) {
const status = (error as any).status;
switch (status) {
case 401:
case 403:
return new AIProviderError(
'Authentication failed. Please check your API key.',
AIErrorType.AUTHENTICATION,
status,
error
);
case 429:
return new AIProviderError(
'Rate limit exceeded. Please try again later.',
AIErrorType.RATE_LIMIT,
status,
error
);
case 404:
return new AIProviderError(
'Model or endpoint not found.',
AIErrorType.MODEL_NOT_FOUND,
status,
error
);
case 400:
return new AIProviderError(
'Invalid request parameters.',
AIErrorType.INVALID_REQUEST,
status,
error
);
}
}
// Handle timeout errors
if (error.message.includes('timeout') || error.message.includes('ETIMEDOUT')) {
return new AIProviderError(
'Request timed out. Please try again.',
AIErrorType.TIMEOUT,
undefined,
error
);
}
// Handle network errors
if (error.message.includes('network') || error.message.includes('ENOTFOUND')) {
return new AIProviderError(
'Network error occurred. Please check your connection.',
AIErrorType.NETWORK,
undefined,
error
);
}
// Default to unknown error
return new AIProviderError(
`Unknown error: ${error.message}`,
AIErrorType.UNKNOWN,
undefined,
error
);
}
}

323
src/providers/claude.ts Normal file
View File

@ -0,0 +1,323 @@
/**
* Claude AI Provider implementation using Anthropic's API
* Provides integration with Claude models through a standardized interface
*/
import Anthropic from '@anthropic-ai/sdk';
import type {
AIProviderConfig,
CompletionParams,
CompletionResponse,
CompletionChunk,
ProviderInfo,
AIMessage
} from '../types/index.js';
import { BaseAIProvider } from './base.js';
import { AIProviderError, AIErrorType } from '../types/index.js';
/**
* Configuration specific to Claude provider
*/
export interface ClaudeConfig extends AIProviderConfig {
/** Default model to use if not specified in requests (default: claude-3-5-sonnet-20241022) */
defaultModel?: string;
/** Anthropic API version (default: 2023-06-01) */
version?: string;
}
/**
* Claude AI provider implementation
*/
export class ClaudeProvider extends BaseAIProvider {
private client: Anthropic | null = null;
private readonly defaultModel: string;
private readonly version: string;
constructor(config: ClaudeConfig) {
super(config);
this.defaultModel = config.defaultModel || 'claude-3-5-sonnet-20241022';
this.version = config.version || '2023-06-01';
}
/**
* Initialize the Claude provider by setting up the Anthropic client
*/
protected async doInitialize(): Promise<void> {
try {
this.client = new Anthropic({
apiKey: this.config.apiKey,
baseURL: this.config.baseUrl,
timeout: this.config.timeout,
maxRetries: this.config.maxRetries,
defaultHeaders: {
'anthropic-version': this.version
}
});
// Test the connection by making a simple request
await this.validateConnection();
} catch (error) {
throw new AIProviderError(
`Failed to initialize Claude provider: ${(error as Error).message}`,
AIErrorType.AUTHENTICATION,
undefined,
error as Error
);
}
}
/**
* Generate a completion using Claude
*/
protected async doComplete(params: CompletionParams): Promise<CompletionResponse> {
if (!this.client) {
throw new AIProviderError('Client not initialized', AIErrorType.INVALID_REQUEST);
}
try {
const { system, messages } = this.convertMessages(params.messages);
const response = await this.client.messages.create({
model: params.model || this.defaultModel,
max_tokens: params.maxTokens || 1000,
temperature: params.temperature ?? 0.7,
top_p: params.topP,
stop_sequences: params.stopSequences,
system: system || undefined,
messages: messages.map(msg => ({
role: msg.role as 'user' | 'assistant',
content: msg.content
}))
});
return this.formatCompletionResponse(response);
} catch (error) {
throw this.handleAnthropicError(error as Error);
}
}
/**
* Generate a streaming completion using Claude
*/
protected async *doStream(params: CompletionParams): AsyncIterable<CompletionChunk> {
if (!this.client) {
throw new AIProviderError('Client not initialized', AIErrorType.INVALID_REQUEST);
}
try {
const { system, messages } = this.convertMessages(params.messages);
const stream = await this.client.messages.create({
model: params.model || this.defaultModel,
max_tokens: params.maxTokens || 1000,
temperature: params.temperature ?? 0.7,
top_p: params.topP,
stop_sequences: params.stopSequences,
system: system || undefined,
messages: messages.map(msg => ({
role: msg.role as 'user' | 'assistant',
content: msg.content
})),
stream: true
});
let content = '';
let messageId = '';
for await (const chunk of stream) {
if (chunk.type === 'message_start') {
messageId = chunk.message.id;
} else if (chunk.type === 'content_block_delta' && chunk.delta.type === 'text_delta') {
content += chunk.delta.text;
yield {
content: chunk.delta.text,
isComplete: false,
id: messageId
};
} else if (chunk.type === 'message_delta' && chunk.usage) {
// Final chunk with usage information
yield {
content: '',
isComplete: true,
id: messageId,
usage: {
promptTokens: chunk.usage.input_tokens || 0,
completionTokens: chunk.usage.output_tokens || 0,
totalTokens: (chunk.usage.input_tokens || 0) + (chunk.usage.output_tokens || 0)
}
};
}
}
} catch (error) {
throw this.handleAnthropicError(error as Error);
}
}
/**
* Get information about the Claude provider
*/
public getInfo(): ProviderInfo {
return {
name: 'Claude',
version: '1.0.0',
models: [
'claude-3-5-sonnet-20241022',
'claude-3-5-haiku-20241022',
'claude-3-opus-20240229',
'claude-3-sonnet-20240229',
'claude-3-haiku-20240307'
],
maxContextLength: 200000, // Claude 3.5 Sonnet context length
supportsStreaming: true,
capabilities: {
vision: true,
functionCalling: true,
systemMessages: true
}
};
}
/**
* Validate the connection by making a simple request
*/
private async validateConnection(): Promise<void> {
if (!this.client) {
throw new Error('Client not initialized');
}
try {
// Make a minimal request to validate credentials
await this.client.messages.create({
model: this.defaultModel,
max_tokens: 1,
messages: [{ role: 'user', content: 'Hi' }]
});
} catch (error: any) {
if (error.status === 401 || error.status === 403) {
throw new AIProviderError(
'Invalid API key. Please check your Anthropic API key.',
AIErrorType.AUTHENTICATION,
error.status
);
}
// For other errors during validation, we'll let initialization proceed
// as they might be temporary issues
}
}
/**
* Convert our generic message format to Claude's format
* Claude requires system messages to be separate from the conversation
*/
private convertMessages(messages: AIMessage[]): { system: string | null; messages: AIMessage[] } {
let system: string | null = null;
const conversationMessages: AIMessage[] = [];
for (const message of messages) {
if (message.role === 'system') {
// Combine multiple system messages
if (system) {
system += '\n\n' + message.content;
} else {
system = message.content;
}
} else {
conversationMessages.push(message);
}
}
return { system, messages: conversationMessages };
}
/**
* Format Anthropic's response to our standard format
*/
private formatCompletionResponse(response: any): CompletionResponse {
const content = response.content
.filter((block: any) => block.type === 'text')
.map((block: any) => block.text)
.join('');
return {
content,
model: response.model,
usage: {
promptTokens: response.usage.input_tokens,
completionTokens: response.usage.output_tokens,
totalTokens: response.usage.input_tokens + response.usage.output_tokens
},
id: response.id,
metadata: {
stopReason: response.stop_reason,
stopSequence: response.stop_sequence
}
};
}
/**
* Handle Anthropic-specific errors and convert them to our standard format
*/
private handleAnthropicError(error: any): AIProviderError {
if (error instanceof AIProviderError) {
return error;
}
const status = error.status || error.statusCode;
const message = error.message || 'Unknown Anthropic API error';
switch (status) {
case 400:
return new AIProviderError(
`Invalid request: ${message}`,
AIErrorType.INVALID_REQUEST,
status,
error
);
case 401:
return new AIProviderError(
'Authentication failed. Please check your Anthropic API key.',
AIErrorType.AUTHENTICATION,
status,
error
);
case 403:
return new AIProviderError(
'Access forbidden. Please check your API key permissions.',
AIErrorType.AUTHENTICATION,
status,
error
);
case 404:
return new AIProviderError(
'Model not found. Please check the model name.',
AIErrorType.MODEL_NOT_FOUND,
status,
error
);
case 429:
return new AIProviderError(
'Rate limit exceeded. Please slow down your requests.',
AIErrorType.RATE_LIMIT,
status,
error
);
case 500:
case 502:
case 503:
case 504:
return new AIProviderError(
'Anthropic service temporarily unavailable. Please try again later.',
AIErrorType.NETWORK,
status,
error
);
default:
return new AIProviderError(
`Anthropic API error: ${message}`,
AIErrorType.UNKNOWN,
status,
error
);
}
}
}

7
src/providers/index.ts Normal file
View File

@ -0,0 +1,7 @@
/**
* Provider exports
* Centralizes all AI provider implementations for easy importing
*/
export { BaseAIProvider } from './base.js';
export { ClaudeProvider, type ClaudeConfig } from './claude.js';

147
src/types/index.ts Normal file
View File

@ -0,0 +1,147 @@
/**
* Core types and interfaces for the Simple AI Provider package
* Defines the contract that all AI providers must implement
*/
/**
* Configuration options for AI providers
*/
export interface AIProviderConfig {
/** API key for the AI service */
apiKey: string;
/** Optional base URL for custom endpoints */
baseUrl?: string;
/** Request timeout in milliseconds (default: 30000) */
timeout?: number;
/** Maximum number of retry attempts (default: 3) */
maxRetries?: number;
/** Additional provider-specific options */
[key: string]: any;
}
/**
* Message role types supported by AI providers
*/
export type MessageRole = 'system' | 'user' | 'assistant';
/**
* Individual message in a conversation
*/
export interface AIMessage {
/** Role of the message sender */
role: MessageRole;
/** Content of the message */
content: string;
/** Optional metadata for the message */
metadata?: Record<string, any>;
}
/**
* Parameters for AI completion requests
*/
export interface CompletionParams {
/** Array of messages forming the conversation */
messages: AIMessage[];
/** Model to use for completion */
model?: string;
/** Maximum tokens to generate (default: 1000) */
maxTokens?: number;
/** Temperature for randomness (0.0 to 1.0, default: 0.7) */
temperature?: number;
/** Top-p for nucleus sampling (default: 1.0) */
topP?: number;
/** Stop sequences to end generation */
stopSequences?: string[];
/** Whether to stream the response */
stream?: boolean;
/** Additional provider-specific parameters */
[key: string]: any;
}
/**
* Usage statistics for a completion
*/
export interface TokenUsage {
/** Number of tokens in the prompt */
promptTokens: number;
/** Number of tokens in the completion */
completionTokens: number;
/** Total number of tokens used */
totalTokens: number;
}
/**
* Response from an AI completion request
*/
export interface CompletionResponse {
/** Generated content */
content: string;
/** Model used for generation */
model: string;
/** Token usage statistics */
usage: TokenUsage;
/** Unique identifier for the request */
id: string;
/** Provider-specific metadata */
metadata?: Record<string, any>;
}
/**
* Streaming chunk from an AI completion
*/
export interface CompletionChunk {
/** Content delta for this chunk */
content: string;
/** Whether this is the final chunk */
isComplete: boolean;
/** Unique identifier for the request */
id: string;
/** Token usage (only available on final chunk) */
usage?: TokenUsage;
}
/**
* Error types that can occur during AI operations
*/
export enum AIErrorType {
AUTHENTICATION = 'authentication',
RATE_LIMIT = 'rate_limit',
INVALID_REQUEST = 'invalid_request',
MODEL_NOT_FOUND = 'model_not_found',
NETWORK = 'network',
TIMEOUT = 'timeout',
UNKNOWN = 'unknown'
}
/**
* Custom error class for AI provider errors
*/
export class AIProviderError extends Error {
constructor(
message: string,
public type: AIErrorType,
public statusCode?: number,
public originalError?: Error
) {
super(message);
this.name = 'AIProviderError';
}
}
/**
* Provider information and capabilities
*/
export interface ProviderInfo {
/** Name of the provider */
name: string;
/** Version of the provider implementation */
version: string;
/** List of available models */
models: string[];
/** Maximum context length supported */
maxContextLength: number;
/** Whether streaming is supported */
supportsStreaming: boolean;
/** Additional capabilities */
capabilities?: Record<string, any>;
}

104
src/utils/factory.ts Normal file
View File

@ -0,0 +1,104 @@
/**
* Factory utilities for creating AI providers
* Provides convenient methods for instantiating and configuring providers
*/
import type { AIProviderConfig } from '../types/index.js';
import { ClaudeProvider, type ClaudeConfig } from '../providers/claude.js';
import { BaseAIProvider } from '../providers/base.js';
/**
* Supported AI provider types
*/
export type ProviderType = 'claude';
/**
* Configuration map for different provider types
*/
export interface ProviderConfigMap {
claude: ClaudeConfig;
}
/**
* Factory function to create AI providers
* @param type - The type of provider to create
* @param config - Configuration for the provider
* @returns Configured AI provider instance
*/
export function createProvider<T extends ProviderType>(
type: T,
config: ProviderConfigMap[T]
): BaseAIProvider {
switch (type) {
case 'claude':
return new ClaudeProvider(config as ClaudeConfig);
default:
throw new Error(`Unsupported provider type: ${type}`);
}
}
/**
* Create a Claude provider with simplified configuration
* @param apiKey - Anthropic API key
* @param options - Optional additional configuration
* @returns Configured Claude provider instance
*/
export function createClaudeProvider(
apiKey: string,
options: Partial<Omit<ClaudeConfig, 'apiKey'>> = {}
): ClaudeProvider {
return new ClaudeProvider({
apiKey,
...options
});
}
/**
* Provider registry for dynamic provider creation
*/
export class ProviderRegistry {
private static providers = new Map<string, new (config: AIProviderConfig) => BaseAIProvider>();
/**
* Register a new provider type
* @param name - Name of the provider
* @param providerClass - Provider class constructor
*/
static register(name: string, providerClass: new (config: AIProviderConfig) => BaseAIProvider): void {
this.providers.set(name.toLowerCase(), providerClass);
}
/**
* Create a provider by name
* @param name - Name of the provider
* @param config - Configuration for the provider
* @returns Provider instance
*/
static create(name: string, config: AIProviderConfig): BaseAIProvider {
const ProviderClass = this.providers.get(name.toLowerCase());
if (!ProviderClass) {
throw new Error(`Provider '${name}' is not registered`);
}
return new ProviderClass(config);
}
/**
* Get list of registered provider names
* @returns Array of registered provider names
*/
static getRegisteredProviders(): string[] {
return Array.from(this.providers.keys());
}
/**
* Check if a provider is registered
* @param name - Name of the provider
* @returns True if provider is registered
*/
static isRegistered(name: string): boolean {
return this.providers.has(name.toLowerCase());
}
}
// Pre-register built-in providers
ProviderRegistry.register('claude', ClaudeProvider);

138
tests/claude.test.ts Normal file
View File

@ -0,0 +1,138 @@
/**
* Tests for Claude Provider
*/
import { describe, it, expect, beforeEach } from 'bun:test';
import { ClaudeProvider, AIProviderError, AIErrorType } from '../src/index.js';
describe('ClaudeProvider', () => {
let provider: ClaudeProvider;
beforeEach(() => {
provider = new ClaudeProvider({
apiKey: 'test-api-key',
defaultModel: 'claude-3-5-haiku-20241022'
});
});
describe('constructor', () => {
it('should create provider with valid config', () => {
expect(provider).toBeInstanceOf(ClaudeProvider);
expect(provider.isInitialized()).toBe(false);
});
it('should throw error for missing API key', () => {
expect(() => {
new ClaudeProvider({ apiKey: '' });
}).toThrow(AIProviderError);
});
});
describe('getInfo', () => {
it('should return provider information', () => {
const info = provider.getInfo();
expect(info.name).toBe('Claude');
expect(info.version).toBe('1.0.0');
expect(info.supportsStreaming).toBe(true);
expect(info.models).toContain('claude-3-5-sonnet-20241022');
expect(info.maxContextLength).toBe(200000);
});
});
describe('validation', () => {
it('should validate temperature range', async () => {
// Mock initialization to avoid API call
(provider as any).initialized = true;
(provider as any).client = {};
await expect(
provider.complete({
messages: [{ role: 'user', content: 'test' }],
temperature: 1.5
})
).rejects.toThrow('Temperature must be between 0.0 and 1.0');
});
it('should validate message format', async () => {
(provider as any).initialized = true;
(provider as any).client = {};
await expect(
provider.complete({
messages: [{ role: 'invalid' as any, content: 'test' }]
})
).rejects.toThrow('Each message must have a valid role');
});
it('should require initialization before use', async () => {
await expect(
provider.complete({
messages: [{ role: 'user', content: 'test' }]
})
).rejects.toThrow('Provider must be initialized before use');
});
});
describe('error handling', () => {
it('should handle authentication errors', () => {
const error = new Error('Unauthorized');
(error as any).status = 401;
const providerError = (provider as any).handleAnthropicError(error);
expect(providerError).toBeInstanceOf(AIProviderError);
expect(providerError.type).toBe(AIErrorType.AUTHENTICATION);
});
it('should handle rate limit errors', () => {
const error = new Error('Rate limited');
(error as any).status = 429;
const providerError = (provider as any).handleAnthropicError(error);
expect(providerError).toBeInstanceOf(AIProviderError);
expect(providerError.type).toBe(AIErrorType.RATE_LIMIT);
});
it('should handle model not found errors', () => {
const error = new Error('Model not found');
(error as any).status = 404;
const providerError = (provider as any).handleAnthropicError(error);
expect(providerError).toBeInstanceOf(AIProviderError);
expect(providerError.type).toBe(AIErrorType.MODEL_NOT_FOUND);
});
});
describe('message conversion', () => {
it('should separate system messages from conversation', () => {
const messages = [
{ role: 'system' as const, content: 'You are helpful' },
{ role: 'user' as const, content: 'Hello' },
{ role: 'assistant' as const, content: 'Hi there' },
{ role: 'system' as const, content: 'Be concise' }
];
const result = (provider as any).convertMessages(messages);
expect(result.system).toBe('You are helpful\n\nBe concise');
expect(result.messages).toHaveLength(2);
expect(result.messages[0].role).toBe('user');
expect(result.messages[1].role).toBe('assistant');
});
it('should handle messages without system prompts', () => {
const messages = [
{ role: 'user' as const, content: 'Hello' },
{ role: 'assistant' as const, content: 'Hi there' }
];
const result = (provider as any).convertMessages(messages);
expect(result.system).toBeNull();
expect(result.messages).toHaveLength(2);
});
});
});

21
tsconfig.build.json Normal file
View File

@ -0,0 +1,21 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": false,
"emitDeclarationOnly": true,
"declaration": true,
"outDir": "./dist",
"moduleResolution": "node",
"verbatimModuleSyntax": false
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist",
"tests",
"**/*.test.ts",
"**/*.spec.ts"
]
}

36
tsconfig.json Normal file
View File

@ -0,0 +1,36 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Module resolution
"moduleResolution": "bundler",
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist",
"tests"
]
}