feat: add initial implementation of Simple AI Provider package
This commit is contained in:
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal 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
32
.npmignore
Normal 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
284
README.md
Normal 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
35
bun.lock
Normal 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
82
examples/basic-usage.ts
Normal 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
57
package.json
Normal 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
52
src/index.ts
Normal 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
264
src/providers/base.ts
Normal 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
323
src/providers/claude.ts
Normal 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
7
src/providers/index.ts
Normal 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
147
src/types/index.ts
Normal 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
104
src/utils/factory.ts
Normal 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
138
tests/claude.test.ts
Normal 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
21
tsconfig.build.json
Normal 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
36
tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
Reference in New Issue
Block a user