Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
b99707c629
|
|||
|
b935209142
|
|||
|
d4871d22fe
|
|||
|
9cb7ee8f9e
|
|||
|
a0d181787c
|
|||
|
93ef2611c0
|
|||
|
c8c579da8b
|
|||
|
e10ce7f53a
|
727
README.md
727
README.md
@@ -1,35 +1,10 @@
|
|||||||
# Simple AI Provider
|
# Simple AI Provider
|
||||||
|
|
||||||
A professional, type-safe TypeScript package that provides a unified interface for multiple AI providers. Currently supports **Claude (Anthropic)**, **OpenAI**, **Google Gemini**, and **OpenWebUI** with a consistent API across all providers.
|
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).
|
||||||
|
|
||||||
## ✨ Features
|
The same `complete()` / `stream()` interface works across every provider, with consistent error types, streaming, and optional structured (typed JSON) output.
|
||||||
|
|
||||||
- 🔗 **Unified Interface**: Same API for Claude, OpenAI, Gemini, and OpenWebUI
|
## Install
|
||||||
- 🎯 **Type Safety**: Full TypeScript support with comprehensive type definitions
|
|
||||||
- 🚀 **Streaming Support**: Real-time response streaming for all providers
|
|
||||||
- 🛡️ **Error Handling**: Standardized error types with provider-specific details
|
|
||||||
- 🏭 **Factory Pattern**: Easy provider creation and management
|
|
||||||
- 🔧 **Configurable**: Extensive configuration options for each provider
|
|
||||||
- 📦 **Zero Dependencies**: Lightweight with minimal external dependencies
|
|
||||||
- 🌐 **Local Support**: OpenWebUI integration for local/private AI models
|
|
||||||
- 🎨 **Structured Output**: Define custom response types for type-safe AI outputs
|
|
||||||
- 🏗️ **Provider Registry**: Dynamic provider registration and creation system
|
|
||||||
- ✅ **Comprehensive Testing**: Full test coverage with Bun test framework
|
|
||||||
- 🔍 **Advanced Validation**: Input validation with detailed error messages
|
|
||||||
|
|
||||||
## 🏗️ Architecture
|
|
||||||
|
|
||||||
The library is built on solid design principles:
|
|
||||||
|
|
||||||
- **Template Method Pattern**: Base provider defines the workflow, subclasses implement specifics
|
|
||||||
- **Factory Pattern**: Clean provider creation and management
|
|
||||||
- **Strategy Pattern**: Unified interface across different AI providers
|
|
||||||
- **Type Safety**: Comprehensive TypeScript support throughout
|
|
||||||
- **Error Normalization**: Consistent error handling across all providers
|
|
||||||
- **Validation First**: Input validation before processing
|
|
||||||
- **Extensibility**: Easy to add new providers via registry system
|
|
||||||
|
|
||||||
## 🚀 Quick Start
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install simple-ai-provider
|
npm install simple-ai-provider
|
||||||
@@ -37,37 +12,14 @@ npm install simple-ai-provider
|
|||||||
bun add simple-ai-provider
|
bun add simple-ai-provider
|
||||||
```
|
```
|
||||||
|
|
||||||
### Basic Usage
|
Requires Node ≥ 18 (or Bun). TypeScript ≥ 5 is recommended as a peer dependency.
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { ClaudeProvider, OpenAIProvider, GeminiProvider, OpenWebUIProvider } from 'simple-ai-provider';
|
import { ClaudeProvider } from 'simple-ai-provider';
|
||||||
|
|
||||||
// Claude
|
const claude = new ClaudeProvider({ apiKey: process.env.ANTHROPIC_API_KEY! });
|
||||||
const claude = new ClaudeProvider({
|
|
||||||
apiKey: process.env.ANTHROPIC_API_KEY!,
|
|
||||||
defaultModel: 'claude-3-5-sonnet-20241022'
|
|
||||||
});
|
|
||||||
|
|
||||||
// OpenAI
|
|
||||||
const openai = new OpenAIProvider({
|
|
||||||
apiKey: process.env.OPENAI_API_KEY!,
|
|
||||||
defaultModel: 'gpt-4o'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Google Gemini
|
|
||||||
const gemini = new GeminiProvider({
|
|
||||||
apiKey: process.env.GOOGLE_AI_API_KEY!,
|
|
||||||
defaultModel: 'gemini-1.5-flash'
|
|
||||||
});
|
|
||||||
|
|
||||||
// OpenWebUI (local)
|
|
||||||
const openwebui = new OpenWebUIProvider({
|
|
||||||
apiKey: 'ollama', // Often not required
|
|
||||||
baseUrl: 'http://localhost:3000',
|
|
||||||
defaultModel: 'llama2'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize and use any provider
|
|
||||||
await claude.initialize();
|
await claude.initialize();
|
||||||
|
|
||||||
const response = await claude.complete({
|
const response = await claude.complete({
|
||||||
@@ -75,183 +27,107 @@ const response = await claude.complete({
|
|||||||
{ role: 'system', content: 'You are a helpful assistant.' },
|
{ role: 'system', content: 'You are a helpful assistant.' },
|
||||||
{ role: 'user', content: 'Explain TypeScript in one sentence.' }
|
{ role: 'user', content: 'Explain TypeScript in one sentence.' }
|
||||||
],
|
],
|
||||||
maxTokens: 100,
|
maxTokens: 200
|
||||||
temperature: 0.7
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(response.content);
|
console.log(response.content);
|
||||||
|
console.log(response.usage); // { promptTokens, completionTokens, totalTokens }
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🏭 Factory Functions
|
Every provider follows the same pattern: construct, `initialize()`, then `complete()` or `stream()`.
|
||||||
|
|
||||||
Create providers using factory functions for cleaner code:
|
## Providers
|
||||||
|
|
||||||
|
### Claude (Anthropic API)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { createProvider, createClaudeProvider, createOpenAIProvider, createGeminiProvider, createOpenWebUIProvider } from 'simple-ai-provider';
|
import { ClaudeProvider } from 'simple-ai-provider';
|
||||||
|
|
||||||
// Method 1: Specific factory functions
|
const claude = new ClaudeProvider({
|
||||||
const claude = createClaudeProvider('your-key', { defaultModel: 'claude-3-5-sonnet-20241022' });
|
apiKey: process.env.ANTHROPIC_API_KEY!,
|
||||||
const openai = createOpenAIProvider('your-key', { defaultModel: 'gpt-4o' });
|
defaultModel: 'claude-3-5-sonnet-20241022', // optional
|
||||||
const gemini = createGeminiProvider('your-key', { defaultModel: 'gemini-1.5-flash' });
|
version: '2023-06-01', // optional
|
||||||
const openwebui = createOpenWebUIProvider({ apiKey: 'your-key', baseUrl: 'http://localhost:3000' });
|
timeout: 30_000, // optional, ms
|
||||||
|
maxRetries: 3 // optional
|
||||||
// Method 2: Generic factory
|
|
||||||
const provider = createProvider('claude', { apiKey: 'your-key' });
|
|
||||||
```
|
|
||||||
|
|
||||||
### Provider Registry
|
|
||||||
|
|
||||||
For dynamic provider creation and registration:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { ProviderRegistry, ClaudeProvider } from 'simple-ai-provider';
|
|
||||||
|
|
||||||
// Register a custom provider
|
|
||||||
ProviderRegistry.register('my-claude', ClaudeProvider);
|
|
||||||
|
|
||||||
// Create provider dynamically
|
|
||||||
const provider = ProviderRegistry.create('my-claude', { apiKey: 'your-key' });
|
|
||||||
|
|
||||||
// Check available providers
|
|
||||||
const availableProviders = ProviderRegistry.getRegisteredProviders();
|
|
||||||
console.log(availableProviders); // ['claude', 'openai', 'gemini', 'openwebui', 'my-claude']
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🎨 Structured Response Types
|
|
||||||
|
|
||||||
Define custom response types for type-safe, structured AI outputs. The library automatically parses the AI's response into your desired type.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { createResponseType, createClaudeProvider } from 'simple-ai-provider';
|
|
||||||
|
|
||||||
// 1. Define your response type
|
|
||||||
interface ProductAnalysis {
|
|
||||||
productName: string;
|
|
||||||
priceRange: 'budget' | 'mid-range' | 'premium';
|
|
||||||
pros: string[];
|
|
||||||
cons: string[];
|
|
||||||
overallRating: number; // 1-10 scale
|
|
||||||
recommendation: 'buy' | 'consider' | 'avoid';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Create a ResponseType object
|
|
||||||
const productAnalysisType = createResponseType<ProductAnalysis>(
|
|
||||||
'A comprehensive product analysis with pros, cons, rating, and recommendation'
|
|
||||||
);
|
|
||||||
|
|
||||||
// 3. Use with any provider
|
|
||||||
const claude = createClaudeProvider({ apiKey: 'your-key' });
|
|
||||||
await claude.initialize();
|
|
||||||
|
|
||||||
const response = await claude.complete<ProductAnalysis>({
|
|
||||||
messages: [
|
|
||||||
{ role: 'user', content: 'Analyze the iPhone 15 Pro from a consumer perspective.' }
|
|
||||||
],
|
|
||||||
responseType: productAnalysisType,
|
|
||||||
maxTokens: 800
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4. Get the fully typed and parsed response
|
|
||||||
const analysis = response.content;
|
|
||||||
console.log(`Product: ${analysis.productName}`);
|
|
||||||
console.log(`Recommendation: ${analysis.recommendation}`);
|
|
||||||
console.log(`Rating: ${analysis.overallRating}/10`);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Key Benefits
|
### Claude Code (subscription or API key via local CLI)
|
||||||
|
|
||||||
- **Automatic Parsing**: The AI's JSON response is automatically parsed into your specified type.
|
`ClaudeCodeProvider` wraps `@anthropic-ai/claude-agent-sdk`, which spawns the local `claude` CLI under the hood. It supports two billing modes:
|
||||||
- **Type Safety**: Get fully typed responses from AI providers with IntelliSense.
|
|
||||||
- **Automatic Prompting**: System prompts are automatically generated to guide the AI.
|
|
||||||
- **Validation**: Built-in response validation and parsing logic.
|
|
||||||
- **Consistency**: Ensures AI outputs match your expected format.
|
|
||||||
- **Developer Experience**: Catch errors at compile-time instead of runtime.
|
|
||||||
|
|
||||||
### Streaming with Response Types
|
**Mode A — Claude Pro/Max subscription** (billed against subscription credits):
|
||||||
|
|
||||||
You can also use response types with streaming. The raw stream provides real-time text, and you can parse the final string once the stream is complete.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { parseAndValidateResponseType } from 'simple-ai-provider';
|
|
||||||
|
|
||||||
const stream = claude.stream({
|
|
||||||
messages: [{ role: 'user', content: 'Analyze the Tesla Model 3.' }],
|
|
||||||
responseType: productAnalysisType,
|
|
||||||
maxTokens: 600
|
|
||||||
});
|
|
||||||
|
|
||||||
let fullResponse = '';
|
|
||||||
for await (const chunk of stream) {
|
|
||||||
if (!chunk.isComplete) {
|
|
||||||
process.stdout.write(chunk.content);
|
|
||||||
fullResponse += chunk.content;
|
|
||||||
} else {
|
|
||||||
console.log('\n\nStream complete!');
|
|
||||||
// Validate the complete streamed response
|
|
||||||
try {
|
|
||||||
const analysis = parseAndValidateResponseType(fullResponse, productAnalysisType);
|
|
||||||
console.log('Validation successful!');
|
|
||||||
console.log(`Product: ${analysis.productName}`);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Validation failed:', (e as Error).message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📝 Environment Variables
|
|
||||||
|
|
||||||
Set up your API keys:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Required for respective providers
|
# One-time: install the CLI, then mint a long-lived OAuth token.
|
||||||
export ANTHROPIC_API_KEY="your-claude-api-key"
|
# `claude login` alone is NOT enough — Anthropic gates SDK use behind setup-token.
|
||||||
export OPENAI_API_KEY="your-openai-api-key"
|
claude setup-token
|
||||||
export GOOGLE_AI_API_KEY="your-gemini-api-key"
|
|
||||||
|
|
||||||
# OpenWebUI Bearer Token (get from Settings > Account in OpenWebUI)
|
|
||||||
export OPENWEBUI_API_KEY="your-bearer-token"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔧 Provider-Specific Configuration
|
|
||||||
|
|
||||||
### Claude Configuration
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const claude = new ClaudeProvider({
|
import { ClaudeCodeProvider } from 'simple-ai-provider';
|
||||||
apiKey: 'your-api-key',
|
|
||||||
defaultModel: 'claude-3-5-sonnet-20241022',
|
const claude = new ClaudeCodeProvider({
|
||||||
version: '2023-06-01',
|
oauthToken: process.env.CLAUDE_CODE_OAUTH_TOKEN // from `claude setup-token`
|
||||||
maxRetries: 3,
|
});
|
||||||
timeout: 30000
|
|
||||||
|
await claude.initialize();
|
||||||
|
const response = await claude.complete({
|
||||||
|
messages: [{ role: 'user', content: 'Hello!' }]
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
### OpenAI Configuration
|
**Mode B — Console API key** (billed per-token):
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
|
const claude = new ClaudeCodeProvider({
|
||||||
|
apiKey: process.env.ANTHROPIC_API_KEY
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
If both are present, `oauthToken` wins. Either field can also be picked up from the environment (`CLAUDE_CODE_OAUTH_TOKEN` / `ANTHROPIC_API_KEY`) without being passed explicitly.
|
||||||
|
|
||||||
|
Optional config:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
new ClaudeCodeProvider({
|
||||||
|
oauthToken: '...',
|
||||||
|
defaultModel: 'sonnet', // 'sonnet' | 'opus' | 'haiku' | 'inherit' | full model ID
|
||||||
|
maxTurns: 1, // 1 for plain completion; raise for agent/tool loops
|
||||||
|
allowedTools: [], // tool names to enable (default: none)
|
||||||
|
cwd: process.cwd() // working directory for the agent
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Trade-offs to know:**
|
||||||
|
- Requires `claude` CLI installed on the host. Not ideal for typical server deployments.
|
||||||
|
- Higher latency than the direct API (spawns a CLI process per request).
|
||||||
|
- Streaming yields text as the SDK emits successive assistant messages, not token-by-token deltas.
|
||||||
|
|
||||||
|
### OpenAI
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { OpenAIProvider } from 'simple-ai-provider';
|
||||||
|
|
||||||
const openai = new OpenAIProvider({
|
const openai = new OpenAIProvider({
|
||||||
apiKey: 'your-api-key',
|
apiKey: process.env.OPENAI_API_KEY!,
|
||||||
defaultModel: 'gpt-4o',
|
defaultModel: 'gpt-4o',
|
||||||
organization: 'your-org-id',
|
organization: 'org-...', // optional
|
||||||
project: 'your-project-id',
|
project: 'proj-...' // optional
|
||||||
maxRetries: 3,
|
|
||||||
timeout: 30000
|
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
### Gemini Configuration
|
`baseUrl` is supported for OpenAI-compatible endpoints.
|
||||||
|
|
||||||
|
### Gemini (Google)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
|
import { GeminiProvider } from 'simple-ai-provider';
|
||||||
|
|
||||||
const gemini = new GeminiProvider({
|
const gemini = new GeminiProvider({
|
||||||
apiKey: 'your-api-key',
|
apiKey: process.env.GOOGLE_AI_API_KEY!,
|
||||||
defaultModel: 'gemini-1.5-flash',
|
defaultModel: 'gemini-2.5-flash',
|
||||||
safetySettings: [
|
safetySettings: [/* SafetySetting[] from @google/genai */],
|
||||||
{
|
|
||||||
category: 'HARM_CATEGORY_HARASSMENT',
|
|
||||||
threshold: 'BLOCK_MEDIUM_AND_ABOVE'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
generationConfig: {
|
generationConfig: {
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
topP: 0.8,
|
topP: 0.8,
|
||||||
@@ -261,441 +137,198 @@ const gemini = new GeminiProvider({
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
### OpenWebUI Configuration
|
Backed by `@google/genai` (the successor to the deprecated `@google/generative-ai`).
|
||||||
|
|
||||||
|
### OpenWebUI (local / self-hosted)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
|
import { OpenWebUIProvider } from 'simple-ai-provider';
|
||||||
|
|
||||||
const openwebui = new OpenWebUIProvider({
|
const openwebui = new OpenWebUIProvider({
|
||||||
apiKey: 'your-bearer-token', // Get from OpenWebUI Settings > Account
|
apiKey: 'your-bearer-token', // from Settings > Account in OpenWebUI
|
||||||
baseUrl: 'http://localhost:3000', // Your OpenWebUI instance
|
baseUrl: 'http://localhost:3000',
|
||||||
defaultModel: 'llama3.1',
|
defaultModel: 'llama3.1:latest',
|
||||||
useOllamaProxy: false, // Use OpenWebUI's chat API (recommended)
|
useOllamaProxy: false, // false: OpenWebUI chat API (default)
|
||||||
// useOllamaProxy: true, // Use Ollama API proxy for direct model access
|
// true: direct Ollama proxy
|
||||||
dangerouslyAllowInsecureConnections: true, // For local HTTPS
|
dangerouslyAllowInsecureConnections: true
|
||||||
timeout: 60000, // Longer timeout for local inference
|
|
||||||
maxRetries: 2
|
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🌊 Streaming Support
|
`useOllamaProxy` flips between two internal strategies — the OpenAI-compatible chat completions endpoint and the direct Ollama generate endpoint.
|
||||||
|
|
||||||
All providers support real-time streaming:
|
## Streaming
|
||||||
|
|
||||||
|
Identical shape across providers:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const stream = provider.stream({
|
for await (const chunk of provider.stream({ messages, maxTokens: 200 })) {
|
||||||
messages: [{ role: 'user', content: 'Count from 1 to 10' }],
|
|
||||||
maxTokens: 100
|
|
||||||
});
|
|
||||||
|
|
||||||
for await (const chunk of stream) {
|
|
||||||
if (!chunk.isComplete) {
|
if (!chunk.isComplete) {
|
||||||
process.stdout.write(chunk.content);
|
process.stdout.write(chunk.content);
|
||||||
} else {
|
} else {
|
||||||
console.log('\nDone! Usage:', chunk.usage);
|
console.log('\n', chunk.usage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔀 Multi-Provider Usage
|
## Structured output
|
||||||
|
|
||||||
Use multiple providers seamlessly:
|
Ask any provider for a typed JSON response. The library injects a system prompt describing the expected shape and parses the result.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const providers = {
|
import { createResponseType } from 'simple-ai-provider';
|
||||||
claude: new ClaudeProvider({ apiKey: process.env.ANTHROPIC_API_KEY! }),
|
|
||||||
openai: new OpenAIProvider({ apiKey: process.env.OPENAI_API_KEY! }),
|
|
||||||
gemini: new GeminiProvider({ apiKey: process.env.GOOGLE_AI_API_KEY! }),
|
|
||||||
openwebui: new OpenWebUIProvider({
|
|
||||||
apiKey: 'ollama',
|
|
||||||
baseUrl: 'http://localhost:3000'
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize all providers
|
interface UserProfile {
|
||||||
await Promise.all(Object.values(providers).map(p => p.initialize()));
|
name: string;
|
||||||
|
age: number;
|
||||||
// Use the same interface for all
|
hobbies: string[];
|
||||||
const prompt = {
|
|
||||||
messages: [{ role: 'user', content: 'Hello!' }],
|
|
||||||
maxTokens: 50
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const [name, provider] of Object.entries(providers)) {
|
|
||||||
try {
|
|
||||||
const response = await provider.complete(prompt);
|
|
||||||
console.log(`${name}: ${response.content}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`${name} failed: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📊 Provider Comparison
|
This also works with `stream()` — chunks deliver text, then the final chunk parses.
|
||||||
|
|
||||||
| Provider | Context Length | Streaming | Vision | Function Calling | Local Execution | Best For |
|
## Factory functions
|
||||||
|----------|---------------|-----------|--------|------------------|-----------------|----------|
|
|
||||||
| **Claude** | 200K tokens | ✅ | ✅ | ✅ | ❌ | Reasoning, Analysis, Code Review |
|
|
||||||
| **OpenAI** | 128K tokens | ✅ | ✅ | ✅ | ❌ | General Purpose, Function Calling |
|
|
||||||
| **Gemini** | 1M tokens | ✅ | ✅ | ✅ | ❌ | Large Documents, Multimodal |
|
|
||||||
| **OpenWebUI** | 32K tokens | ✅ | ❌ | ❌ | ✅ | Privacy, Custom Models, Local |
|
|
||||||
|
|
||||||
### Detailed Capabilities
|
If you prefer a single entry point:
|
||||||
|
|
||||||
Each provider offers unique capabilities:
|
|
||||||
|
|
||||||
#### Claude (Anthropic)
|
|
||||||
|
|
||||||
- Advanced reasoning and analysis
|
|
||||||
- Excellent code review capabilities
|
|
||||||
- Strong safety features
|
|
||||||
- System message support
|
|
||||||
|
|
||||||
#### OpenAI
|
|
||||||
|
|
||||||
- Broad model selection
|
|
||||||
- Function calling support
|
|
||||||
- JSON mode for structured outputs
|
|
||||||
- Vision capabilities
|
|
||||||
|
|
||||||
#### Gemini (Google)
|
|
||||||
|
|
||||||
- Largest context window (1M tokens)
|
|
||||||
- Multimodal capabilities
|
|
||||||
- Cost-effective pricing
|
|
||||||
- Strong multilingual support
|
|
||||||
|
|
||||||
#### OpenWebUI
|
|
||||||
|
|
||||||
- Complete privacy (local execution)
|
|
||||||
- Custom model support
|
|
||||||
- No API costs
|
|
||||||
- RAG (Retrieval Augmented Generation) support
|
|
||||||
|
|
||||||
## 🎯 Model Selection
|
|
||||||
|
|
||||||
### Getting Available Models
|
|
||||||
|
|
||||||
Instead of maintaining a static list, you can programmatically get available models:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Get provider information including available models
|
import { createProvider } from 'simple-ai-provider';
|
||||||
const info = provider.getInfo();
|
|
||||||
console.log('Available models:', info.models);
|
|
||||||
|
|
||||||
// Example output:
|
const claude = createProvider('claude', { apiKey: '…' });
|
||||||
// Claude: ['claude-3-5-sonnet-20241022', 'claude-3-5-haiku-20241022', ...]
|
const openai = createProvider('openai', { apiKey: '…' });
|
||||||
// OpenAI: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', ...]
|
const gemini = createProvider('gemini', { apiKey: '…' });
|
||||||
// Gemini: ['gemini-1.5-flash', 'gemini-1.5-pro', ...]
|
const openwebui = createProvider('openwebui', { apiKey: '…', baseUrl: '…' });
|
||||||
// OpenWebUI: ['llama3.1:latest', 'mistral:latest', ...]
|
const claudeCode = createProvider('claude-code', {});
|
||||||
```
|
```
|
||||||
|
|
||||||
### Model Selection Guidelines
|
Per-provider shortcut factories also exist: `createClaudeProvider`, `createOpenAIProvider`, `createGeminiProvider`, `createOpenWebUIProvider`, `createClaudeCodeProvider`.
|
||||||
|
|
||||||
**For Claude (Anthropic):**
|
## Error handling
|
||||||
- Check [Anthropic's model documentation](https://docs.anthropic.com/claude/docs/models-overview) for latest models
|
|
||||||
|
|
||||||
**For OpenAI:**
|
Every error thrown by the library is an `AIProviderError` with a typed `.type` and optional `.statusCode`:
|
||||||
- Check [OpenAI's model documentation](https://platform.openai.com/docs/models) for latest models
|
|
||||||
|
|
||||||
**For Gemini (Google):**
|
|
||||||
- Check [Google AI's model documentation](https://ai.google.dev/docs/models) for latest models
|
|
||||||
|
|
||||||
**For OpenWebUI:**
|
|
||||||
- Models depend on your local installation
|
|
||||||
- Check your OpenWebUI instance for available models
|
|
||||||
|
|
||||||
## 🚨 Error Handling
|
|
||||||
|
|
||||||
The package provides comprehensive, standardized error handling with detailed error types:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { AIProviderError, AIErrorType } from 'simple-ai-provider';
|
import { AIProviderError, AIErrorType } from 'simple-ai-provider';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await provider.complete({
|
await provider.complete({ messages: [...] });
|
||||||
messages: [{ role: 'user', content: 'Hello' }]
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof AIProviderError) {
|
if (error instanceof AIProviderError) {
|
||||||
switch (error.type) {
|
switch (error.type) {
|
||||||
case AIErrorType.AUTHENTICATION:
|
case AIErrorType.AUTHENTICATION: /* bad API key, expired token */ break;
|
||||||
console.log('Invalid API key or authentication failed');
|
case AIErrorType.RATE_LIMIT: /* slow down */ break;
|
||||||
break;
|
case AIErrorType.MODEL_NOT_FOUND: /* wrong model name */ break;
|
||||||
case AIErrorType.RATE_LIMIT:
|
case AIErrorType.INVALID_REQUEST: /* bad input */ break;
|
||||||
console.log('Rate limited, try again later');
|
case AIErrorType.NETWORK: /* transient connectivity */ break;
|
||||||
break;
|
case AIErrorType.TIMEOUT: /* slow request */ break;
|
||||||
case AIErrorType.MODEL_NOT_FOUND:
|
case AIErrorType.UNKNOWN: /* fall back */ break;
|
||||||
console.log('Model not available or not found');
|
|
||||||
break;
|
|
||||||
case AIErrorType.INVALID_REQUEST:
|
|
||||||
console.log('Invalid request parameters');
|
|
||||||
break;
|
|
||||||
case AIErrorType.NETWORK:
|
|
||||||
console.log('Network/connection issue');
|
|
||||||
break;
|
|
||||||
case AIErrorType.TIMEOUT:
|
|
||||||
console.log('Request timed out');
|
|
||||||
break;
|
|
||||||
case AIErrorType.UNKNOWN:
|
|
||||||
console.log('Unknown error:', error.message);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.log('Error:', error.message);
|
|
||||||
}
|
}
|
||||||
|
console.error(error.statusCode, error.originalError);
|
||||||
// Access additional error details
|
|
||||||
console.log('Status Code:', error.statusCode);
|
|
||||||
console.log('Original Error:', error.originalError);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Error Types
|
## Configuration reference
|
||||||
|
|
||||||
- **AUTHENTICATION**: Invalid API keys or authentication failures
|
All providers share these base options:
|
||||||
- **RATE_LIMIT**: API rate limits exceeded
|
|
||||||
- **INVALID_REQUEST**: Malformed requests or invalid parameters
|
|
||||||
- **MODEL_NOT_FOUND**: Requested model is not available
|
|
||||||
- **NETWORK**: Connection issues or server errors
|
|
||||||
- **TIMEOUT**: Request timeout exceeded
|
|
||||||
- **UNKNOWN**: Unclassified errors
|
|
||||||
|
|
||||||
## 🔧 Advanced Usage
|
| 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 |
|
||||||
|
|
||||||
### Custom Base URLs
|
Each provider adds its own config (see the sections above).
|
||||||
|
|
||||||
```typescript
|
## TypeScript
|
||||||
// OpenAI-compatible endpoint
|
|
||||||
const customOpenAI = new OpenAIProvider({
|
|
||||||
apiKey: 'your-key',
|
|
||||||
baseUrl: 'https://api.custom-provider.com/v1'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Custom OpenWebUI instance
|
Full type definitions ship with the package. The main types you'll use:
|
||||||
const remoteOpenWebUI = new OpenWebUIProvider({
|
|
||||||
apiKey: 'your-key',
|
|
||||||
baseUrl: 'https://my-openwebui.example.com',
|
|
||||||
apiPath: '/api/v1'
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Provider Information
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const info = provider.getInfo();
|
|
||||||
console.log(`Provider: ${info.name} v${info.version}`);
|
|
||||||
console.log(`Models: ${info.models.join(', ')}`);
|
|
||||||
console.log(`Max Context: ${info.maxContextLength} tokens`);
|
|
||||||
console.log(`Supports Streaming: ${info.supportsStreaming}`);
|
|
||||||
console.log('Capabilities:', info.capabilities);
|
|
||||||
```
|
|
||||||
|
|
||||||
### OpenWebUI-Specific Features
|
|
||||||
|
|
||||||
OpenWebUI offers unique advantages for local AI deployment:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const openwebui = new OpenWebUIProvider({
|
|
||||||
apiKey: 'your-bearer-token', // Get from OpenWebUI Settings > Account
|
|
||||||
baseUrl: 'http://localhost:3000',
|
|
||||||
defaultModel: 'llama3.1',
|
|
||||||
useOllamaProxy: false, // Use chat completions API (recommended)
|
|
||||||
// Longer timeout for local inference
|
|
||||||
timeout: 120000,
|
|
||||||
// Allow self-signed certificates for local development
|
|
||||||
dangerouslyAllowInsecureConnections: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test connection and list available models
|
|
||||||
try {
|
|
||||||
await openwebui.initialize();
|
|
||||||
console.log('Connected to local OpenWebUI instance');
|
|
||||||
|
|
||||||
// Use either chat completions or Ollama proxy
|
|
||||||
const response = await openwebui.complete({
|
|
||||||
messages: [{ role: 'user', content: 'Hello!' }],
|
|
||||||
maxTokens: 100
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.log('OpenWebUI not available:', error.message);
|
|
||||||
// Gracefully fallback to cloud providers
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**OpenWebUI API Modes:**
|
|
||||||
|
|
||||||
- **Chat Completions** (`useOllamaProxy: false`): OpenWebUI's native API with full features
|
|
||||||
- **Ollama Proxy** (`useOllamaProxy: true`): Direct access to Ollama API for raw model interaction
|
|
||||||
|
|
||||||
## 📦 TypeScript Support
|
|
||||||
|
|
||||||
Full TypeScript support with comprehensive type definitions:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import type {
|
import type {
|
||||||
|
AIMessage,
|
||||||
CompletionParams,
|
CompletionParams,
|
||||||
CompletionResponse,
|
CompletionResponse,
|
||||||
CompletionChunk,
|
CompletionChunk,
|
||||||
|
TokenUsage,
|
||||||
ProviderInfo,
|
ProviderInfo,
|
||||||
|
ResponseType,
|
||||||
|
|
||||||
|
// Per-provider config interfaces
|
||||||
ClaudeConfig,
|
ClaudeConfig,
|
||||||
|
ClaudeCodeConfig,
|
||||||
OpenAIConfig,
|
OpenAIConfig,
|
||||||
GeminiConfig,
|
GeminiConfig,
|
||||||
OpenWebUIConfig,
|
OpenWebUIConfig
|
||||||
AIMessage,
|
|
||||||
ResponseType,
|
|
||||||
TokenUsage
|
|
||||||
} from 'simple-ai-provider';
|
} from 'simple-ai-provider';
|
||||||
|
|
||||||
// Type-safe configuration
|
|
||||||
const config: ClaudeConfig = {
|
|
||||||
apiKey: 'your-key',
|
|
||||||
defaultModel: 'claude-3-5-sonnet-20241022',
|
|
||||||
// TypeScript will validate all options
|
|
||||||
};
|
|
||||||
|
|
||||||
// Type-safe responses
|
|
||||||
const response: CompletionResponse = await provider.complete(params);
|
|
||||||
|
|
||||||
// Type-safe messages with metadata
|
|
||||||
const messages: AIMessage[] = [
|
|
||||||
{
|
|
||||||
role: 'user',
|
|
||||||
content: 'Hello',
|
|
||||||
metadata: { timestamp: Date.now() }
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// Type-safe response types
|
|
||||||
interface UserProfile {
|
|
||||||
name: string;
|
|
||||||
age: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const responseType: ResponseType<UserProfile> = createResponseType(
|
|
||||||
'A user profile with name and age',
|
|
||||||
{ name: 'John', age: 30 }
|
|
||||||
);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Advanced Type Features
|
`CompletionResponse<T>` is generic — when you pass `responseType`, `content` is `T` (and the original string is preserved on `rawContent`).
|
||||||
|
|
||||||
- **Generic Response Types**: Type-safe structured outputs
|
## Provider metadata
|
||||||
- **Message Metadata**: Support for custom message properties
|
|
||||||
- **Provider-Specific Configs**: Type-safe configuration for each provider
|
|
||||||
- **Error Types**: Comprehensive error type definitions
|
|
||||||
- **Factory Functions**: Type-safe provider creation
|
|
||||||
|
|
||||||
## 🧪 Testing
|
Every provider exposes `getInfo()`:
|
||||||
|
|
||||||
The package includes comprehensive tests using Bun test framework:
|
```typescript
|
||||||
|
const info = provider.getInfo();
|
||||||
```bash
|
// { name, version, models, maxContextLength, supportsStreaming, capabilities }
|
||||||
# Run all tests
|
|
||||||
bun test
|
|
||||||
|
|
||||||
# Run tests for specific provider
|
|
||||||
bun test tests/claude.test.ts
|
|
||||||
bun test tests/openai.test.ts
|
|
||||||
bun test tests/gemini.test.ts
|
|
||||||
bun test tests/openwebui.test.ts
|
|
||||||
|
|
||||||
# Run tests with coverage
|
|
||||||
bun test --coverage
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Test Coverage
|
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.
|
||||||
|
|
||||||
- ✅ Provider initialization and configuration
|
## Architecture (brief)
|
||||||
- ✅ Message validation and conversion
|
|
||||||
- ✅ Error handling and normalization
|
|
||||||
- ✅ Response formatting
|
|
||||||
- ✅ Streaming functionality
|
|
||||||
- ✅ Structured response types
|
|
||||||
- ✅ Factory functions
|
|
||||||
- ✅ Provider registry
|
|
||||||
|
|
||||||
## 🛠️ Development
|
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:
|
||||||
|
|
||||||
### Prerequisites
|
- `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
|
||||||
|
|
||||||
- Node.js 18.0.0 or higher
|
`OpenWebUIProvider` additionally uses an internal strategy split (`OpenWebUIChatStrategy` vs `OpenWebUIOllamaStrategy`) selected by `useOllamaProxy`.
|
||||||
- Bun (recommended) or npm/yarn
|
|
||||||
- TypeScript 5.0 or higher
|
|
||||||
|
|
||||||
### Setup
|
## Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone the repository
|
|
||||||
git clone https://gitea.jleibl.net/jleibl/simple-ai-provider.git
|
git clone https://gitea.jleibl.net/jleibl/simple-ai-provider.git
|
||||||
cd simple-ai-provider
|
cd simple-ai-provider
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
bun install
|
bun install
|
||||||
|
|
||||||
# Build the project
|
|
||||||
bun run build
|
bun run build
|
||||||
|
```
|
||||||
|
|
||||||
# Run tests
|
Examples live in `examples/`:
|
||||||
bun test
|
|
||||||
|
|
||||||
# Run examples
|
```bash
|
||||||
bun run examples/basic-usage.ts
|
bun run examples/basic-usage.ts
|
||||||
bun run examples/structured-response-types.ts
|
|
||||||
bun run examples/multi-provider.ts
|
bun run examples/multi-provider.ts
|
||||||
|
bun run examples/structured-response-types.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
### Project Structure
|
Tests in `tests/` use Bun's test runner (`bun test`). Note: post-refactor some tests need updating before they pass cleanly.
|
||||||
|
|
||||||
```text
|
## License
|
||||||
src/
|
|
||||||
├── index.ts # Main entry point
|
|
||||||
├── types/
|
|
||||||
│ └── index.ts # Type definitions and utilities
|
|
||||||
├── providers/
|
|
||||||
│ ├── base.ts # Abstract base provider
|
|
||||||
│ ├── claude.ts # Claude provider implementation
|
|
||||||
│ ├── openai.ts # OpenAI provider implementation
|
|
||||||
│ ├── gemini.ts # Gemini provider implementation
|
|
||||||
│ ├── openwebui.ts # OpenWebUI provider implementation
|
|
||||||
│ └── index.ts # Provider exports
|
|
||||||
└── utils/
|
|
||||||
└── factory.ts # Factory functions and registry
|
|
||||||
|
|
||||||
examples/
|
MIT — see [LICENSE](LICENSE).
|
||||||
├── basic-usage.ts # Basic usage examples
|
|
||||||
├── structured-response-types.ts # Structured output examples
|
|
||||||
└── multi-provider.ts # Multi-provider examples
|
|
||||||
|
|
||||||
tests/
|
## Links
|
||||||
├── claude.test.ts # Claude provider tests
|
|
||||||
├── openai.test.ts # OpenAI provider tests
|
|
||||||
├── gemini.test.ts # Gemini provider tests
|
|
||||||
└── openwebui.test.ts # OpenWebUI provider tests
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🤝 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.
|
|
||||||
|
|
||||||
### Development Guidelines
|
|
||||||
|
|
||||||
1. **Code Style**: Follow the existing TypeScript patterns
|
|
||||||
2. **Testing**: Add tests for new features
|
|
||||||
3. **Documentation**: Update README for new features
|
|
||||||
4. **Type Safety**: Maintain comprehensive type definitions
|
|
||||||
5. **Error Handling**: Use standardized error types
|
|
||||||
|
|
||||||
## 📄 License
|
|
||||||
|
|
||||||
MIT License - see the [LICENSE](LICENSE) file for details.
|
|
||||||
|
|
||||||
## 🔗 Links
|
|
||||||
|
|
||||||
- [Anthropic Claude API](https://docs.anthropic.com/claude/reference/)
|
- [Anthropic Claude API](https://docs.anthropic.com/claude/reference/)
|
||||||
|
- [Claude Code SDK](https://docs.anthropic.com/claude/docs/claude-code-sdk)
|
||||||
- [OpenAI API](https://platform.openai.com/docs/)
|
- [OpenAI API](https://platform.openai.com/docs/)
|
||||||
- [Google Gemini API](https://ai.google.dev/)
|
- [Google Gen AI SDK](https://ai.google.dev/)
|
||||||
- [OpenWebUI](https://openwebui.com/)
|
- [OpenWebUI](https://openwebui.com/)
|
||||||
- [Gitea Repository](https://gitea.jleibl.net/jleibl/simple-ai-provider)
|
- [Repository](https://gitea.jleibl.net/jleibl/simple-ai-provider)
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
⭐ **Star this repo if you find it helpful!**
|
|
||||||
|
|||||||
199
bun.lock
199
bun.lock
@@ -4,6 +4,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "simple-ai-provider",
|
"name": "simple-ai-provider",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@anthropic-ai/claude-agent-sdk": "^0.3.146",
|
||||||
"@anthropic-ai/sdk": "^0.97.0",
|
"@anthropic-ai/sdk": "^0.97.0",
|
||||||
"@google/genai": "^2.5.0",
|
"@google/genai": "^2.5.0",
|
||||||
"openai": "^6.0.0",
|
"openai": "^6.0.0",
|
||||||
@@ -19,12 +20,34 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
|
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.3.146", "", { "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.146", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.146", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.146", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.146", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.146", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.146", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.146", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.146" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.93.0", "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^4.0.0" } }, "sha512-hK9/Ng+hOyexUemTxdIUsSWJ9o2LFi2YNWzHwz8/YMCohUYOnFMZkBiENvUAb0WIc5hieOyBZrOIlg5OewuJMg=="],
|
||||||
|
|
||||||
|
"@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.146", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0IIvlEaenq2CRSVx5Bo5BaCtHQXS87GancM35WKEYveGVLn6DI+5G7ikYuTE4AKRPkMnogFtY4BJt6LulWGj+A=="],
|
||||||
|
|
||||||
|
"@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.146", "", { "os": "darwin", "cpu": "x64" }, "sha512-Dk5xJ03Ff1JXbMRP1t2wc/TyfY6xF/2Ysp31wMhFPjoNiKSPHMWaIg242+T3CHdxLWmJ8plWHL1HL5cyZ/LCkw=="],
|
||||||
|
|
||||||
|
"@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.146", "", { "os": "linux", "cpu": "arm64" }, "sha512-mzBXDDWWBAC/vDtAYpO1G/dq5QvJtYSPXsqcb+sNdcDhiuf4IYnYp7ytRncYlsUNDkLmX6Gk2jkWAHUUA2Lozg=="],
|
||||||
|
|
||||||
|
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.146", "", { "os": "linux", "cpu": "arm64" }, "sha512-QlCid0ucdrmhUAOewfQjaofN2wlokWcfFTxSFePTSj1umk35JO7TDFP700F7jU49r1fPWIdvJpPwWGyB0DeFPA=="],
|
||||||
|
|
||||||
|
"@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.3.146", "", { "os": "linux", "cpu": "x64" }, "sha512-B2baXU1tCBT5CVlD7jJMKjpC4xdO45NUIWpqImmwuOfKvlM/PITjyTXyTY662mGZf1dBmdqBBsqirwFH/jhi8Q=="],
|
||||||
|
|
||||||
|
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.146", "", { "os": "linux", "cpu": "x64" }, "sha512-E3coK1ThQT08KIX80RLcsq7DWXFllCKOzoOe32it/bdtY56TBgPY9xemwXhIJ+cVBHTI9/MpBSIlKBcFCt+yQA=="],
|
||||||
|
|
||||||
|
"@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.146", "", { "os": "win32", "cpu": "arm64" }, "sha512-CIwQxGX2r/yWpjCJ6ahB3smKXhghWgGTxL98+LGW52TUwqTiBnlNrH9DPqqgv1/+Hyquw6xfLrKU+StyfMgiLw=="],
|
||||||
|
|
||||||
|
"@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.3.146", "", { "os": "win32", "cpu": "x64" }, "sha512-qmxrsyaqA8s4HShqJls7ZCRjdoqN66Jo/hbjQNB3uHepD8tEO1iD19aPV4+osdLT7feMkhDBfLT07Q30R2NB5w=="],
|
||||||
|
|
||||||
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.97.1", "", { "dependencies": { "json-schema-to-ts": "^3.1.1", "standardwebhooks": "^1.0.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-wOf7AUeJPitcVpvKO4UMu63mWH5SaVipkGd7OOQJt/G6VYGlV8D2Gp9dLxOrttDJh/9gqPqdaBwDGcBevumeAg=="],
|
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.97.1", "", { "dependencies": { "json-schema-to-ts": "^3.1.1", "standardwebhooks": "^1.0.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-wOf7AUeJPitcVpvKO4UMu63mWH5SaVipkGd7OOQJt/G6VYGlV8D2Gp9dLxOrttDJh/9gqPqdaBwDGcBevumeAg=="],
|
||||||
|
|
||||||
"@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
|
"@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
|
||||||
|
|
||||||
"@google/genai": ["@google/genai@2.5.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-qDi3LLh9I3llJK0f9uV8kZ8EdT9oHPxGJJ9yOJ/i5YXYrVwRCs8jHo9x4e99uOeKYDvD3TZwT70p/H/LS3BixQ=="],
|
"@google/genai": ["@google/genai@2.5.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-qDi3LLh9I3llJK0f9uV8kZ8EdT9oHPxGJJ9yOJ/i5YXYrVwRCs8jHo9x4e99uOeKYDvD3TZwT70p/H/LS3BixQ=="],
|
||||||
|
|
||||||
|
"@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="],
|
||||||
|
|
||||||
|
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
|
||||||
|
|
||||||
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
|
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
|
||||||
|
|
||||||
"@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
|
"@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
|
||||||
@@ -53,80 +76,256 @@
|
|||||||
|
|
||||||
"@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="],
|
"@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="],
|
||||||
|
|
||||||
|
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||||
|
|
||||||
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
||||||
|
|
||||||
|
"ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
|
||||||
|
|
||||||
|
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||||
|
|
||||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||||
|
|
||||||
"bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
|
"bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
|
||||||
|
|
||||||
|
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
|
||||||
|
|
||||||
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
|
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
|
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
|
||||||
|
|
||||||
|
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||||
|
|
||||||
|
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||||
|
|
||||||
|
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||||
|
|
||||||
|
"content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="],
|
||||||
|
|
||||||
|
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
||||||
|
|
||||||
|
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||||
|
|
||||||
|
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
||||||
|
|
||||||
|
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
|
||||||
|
|
||||||
|
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||||
|
|
||||||
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
|
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
|
||||||
|
|
||||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
|
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||||
|
|
||||||
|
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||||
|
|
||||||
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
|
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
|
||||||
|
|
||||||
|
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||||
|
|
||||||
|
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||||
|
|
||||||
|
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||||
|
|
||||||
|
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||||
|
|
||||||
|
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||||
|
|
||||||
|
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||||
|
|
||||||
|
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
||||||
|
|
||||||
|
"eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
|
||||||
|
|
||||||
|
"eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="],
|
||||||
|
|
||||||
|
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
||||||
|
|
||||||
|
"express-rate-limit": ["express-rate-limit@8.5.2", "", { "dependencies": { "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A=="],
|
||||||
|
|
||||||
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||||
|
|
||||||
|
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||||
|
|
||||||
"fast-sha256": ["fast-sha256@1.3.0", "", {}, "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="],
|
"fast-sha256": ["fast-sha256@1.3.0", "", {}, "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="],
|
||||||
|
|
||||||
|
"fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="],
|
||||||
|
|
||||||
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
|
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
|
||||||
|
|
||||||
|
"finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
|
||||||
|
|
||||||
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
|
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
|
||||||
|
|
||||||
|
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||||
|
|
||||||
|
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
||||||
|
|
||||||
|
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||||
|
|
||||||
"gaxios": ["gaxios@7.1.4", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA=="],
|
"gaxios": ["gaxios@7.1.4", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA=="],
|
||||||
|
|
||||||
"gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="],
|
"gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="],
|
||||||
|
|
||||||
|
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||||
|
|
||||||
|
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||||
|
|
||||||
"google-auth-library": ["google-auth-library@10.6.2", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.1.4", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw=="],
|
"google-auth-library": ["google-auth-library@10.6.2", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.1.4", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw=="],
|
||||||
|
|
||||||
"google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="],
|
"google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="],
|
||||||
|
|
||||||
|
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||||
|
|
||||||
|
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||||
|
|
||||||
|
"hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="],
|
||||||
|
|
||||||
|
"hono": ["hono@4.12.21", "", {}, "sha512-uV63apnb0kyPtAUwoWgaGh9HyIFcv8lgmzPZSiTBQAFOFGIzka5EZ1dZocmGnn0XdX0+XTqJ6Tqv7selMuGLRQ=="],
|
||||||
|
|
||||||
|
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||||
|
|
||||||
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||||
|
|
||||||
|
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
||||||
|
|
||||||
|
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||||
|
|
||||||
|
"ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="],
|
||||||
|
|
||||||
|
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||||
|
|
||||||
|
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
|
||||||
|
|
||||||
|
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||||
|
|
||||||
|
"jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="],
|
||||||
|
|
||||||
"json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
|
"json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
|
||||||
|
|
||||||
"json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
|
"json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
|
||||||
|
|
||||||
|
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||||
|
|
||||||
|
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
|
||||||
|
|
||||||
"jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
|
"jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
|
||||||
|
|
||||||
"jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
|
"jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
|
||||||
|
|
||||||
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
|
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
|
||||||
|
|
||||||
|
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||||
|
|
||||||
|
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
|
||||||
|
|
||||||
|
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
|
||||||
|
|
||||||
|
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||||
|
|
||||||
|
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
||||||
|
|
||||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
|
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||||
|
|
||||||
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
|
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
|
||||||
|
|
||||||
"node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
|
"node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
|
||||||
|
|
||||||
|
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||||
|
|
||||||
|
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||||
|
|
||||||
|
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||||
|
|
||||||
|
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||||
|
|
||||||
"openai": ["openai@6.38.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-AoMplt2UalrpgUDMh3L09QWjNRlgJPipclQvA6sYAaeF6nHNBMgmikAZGmcYLn8on4d9sQY9Q8bOLfrBS7Lc8g=="],
|
"openai": ["openai@6.38.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-AoMplt2UalrpgUDMh3L09QWjNRlgJPipclQvA6sYAaeF6nHNBMgmikAZGmcYLn8on4d9sQY9Q8bOLfrBS7Lc8g=="],
|
||||||
|
|
||||||
"p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="],
|
"p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="],
|
||||||
|
|
||||||
|
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||||
|
|
||||||
|
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||||
|
|
||||||
|
"path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
|
||||||
|
|
||||||
|
"pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
|
||||||
|
|
||||||
"protobufjs": ["protobufjs@7.6.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-LtESOsMPTZgyYtwxhvdgdjGL0HmXEaRA/hVD6sol4zA60hVXXXP/SGmxnqDbgGE8gy7pYex7cym+5vYPcmaXBQ=="],
|
"protobufjs": ["protobufjs@7.6.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-LtESOsMPTZgyYtwxhvdgdjGL0HmXEaRA/hVD6sol4zA60hVXXXP/SGmxnqDbgGE8gy7pYex7cym+5vYPcmaXBQ=="],
|
||||||
|
|
||||||
|
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||||
|
|
||||||
|
"qs": ["qs@6.15.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw=="],
|
||||||
|
|
||||||
|
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||||
|
|
||||||
|
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
||||||
|
|
||||||
|
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||||
|
|
||||||
"retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="],
|
"retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="],
|
||||||
|
|
||||||
|
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
||||||
|
|
||||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||||
|
|
||||||
|
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||||
|
|
||||||
|
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
|
||||||
|
|
||||||
|
"serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
|
||||||
|
|
||||||
|
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||||
|
|
||||||
|
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||||
|
|
||||||
|
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||||
|
|
||||||
|
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
||||||
|
|
||||||
|
"side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="],
|
||||||
|
|
||||||
|
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
||||||
|
|
||||||
|
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||||
|
|
||||||
"standardwebhooks": ["standardwebhooks@1.0.0", "", { "dependencies": { "@stablelib/base64": "^1.0.0", "fast-sha256": "^1.3.0" } }, "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg=="],
|
"standardwebhooks": ["standardwebhooks@1.0.0", "", { "dependencies": { "@stablelib/base64": "^1.0.0", "fast-sha256": "^1.3.0" } }, "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg=="],
|
||||||
|
|
||||||
|
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||||
|
|
||||||
|
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||||
|
|
||||||
"ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="],
|
"ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="],
|
||||||
|
|
||||||
|
"type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="],
|
||||||
|
|
||||||
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
|
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
|
"undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
|
||||||
|
|
||||||
|
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||||
|
|
||||||
|
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||||
|
|
||||||
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
|
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
|
||||||
|
|
||||||
|
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||||
|
|
||||||
|
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||||
|
|
||||||
"ws": ["ws@8.20.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w=="],
|
"ws": ["ws@8.20.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w=="],
|
||||||
|
|
||||||
|
"zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
|
||||||
|
|
||||||
|
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
|
||||||
|
|
||||||
"bun-types/@types/node": ["@types/node@24.12.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="],
|
"bun-types/@types/node": ["@types/node@24.12.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="],
|
||||||
|
|
||||||
|
"type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
|
||||||
|
|
||||||
"bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
"bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
61
examples/claude-code.ts
Normal file
61
examples/claude-code.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* Quick smoke test for ClaudeCodeProvider.
|
||||||
|
*
|
||||||
|
* Prereqs (pick one):
|
||||||
|
* - Claude Pro/Max subscription: run `claude setup-token`, then set
|
||||||
|
* CLAUDE_CODE_OAUTH_TOKEN=<the token it prints> in your environment
|
||||||
|
* (or pass it as `oauthToken` below).
|
||||||
|
* - Console API key: set ANTHROPIC_API_KEY (or pass `apiKey`).
|
||||||
|
*
|
||||||
|
* `claude login` alone is NOT sufficient for SDK use — Anthropic gates
|
||||||
|
* non-interactive (`claude -p`) invocations behind a separate token.
|
||||||
|
*
|
||||||
|
* Run: bun run examples/claude-code.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ClaudeCodeProvider } from '../src/index.js';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const claude = new ClaudeCodeProvider({
|
||||||
|
defaultModel: 'sonnet',
|
||||||
|
oauthToken: process.env.CLAUDE_CODE_OAUTH_TOKEN
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Initializing…');
|
||||||
|
await claude.initialize();
|
||||||
|
console.log('Ready.\n');
|
||||||
|
|
||||||
|
// ── Non-streaming completion ───────────────────────────────────────────
|
||||||
|
console.log('--- complete() ---');
|
||||||
|
const response = await claude.complete({
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: 'You are concise. Reply in one sentence.' },
|
||||||
|
{ role: 'user', content: 'What is the capital of Japan?' }
|
||||||
|
],
|
||||||
|
maxTokens: 100
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(response.content);
|
||||||
|
console.log(`tokens: ${response.usage.totalTokens} | cost: $${response.metadata?.costUsd ?? '?'}\n`);
|
||||||
|
|
||||||
|
// ── Streaming ──────────────────────────────────────────────────────────
|
||||||
|
console.log('--- stream() ---');
|
||||||
|
let totalTokens = 0;
|
||||||
|
for await (const chunk of claude.stream({
|
||||||
|
messages: [{ role: 'user', content: 'Count from 1 to 5, one per line.' }],
|
||||||
|
maxTokens: 100
|
||||||
|
})) {
|
||||||
|
if (!chunk.isComplete) {
|
||||||
|
process.stdout.write(chunk.content);
|
||||||
|
} else {
|
||||||
|
totalTokens = chunk.usage?.totalTokens ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(`\ntokens: ${totalTokens}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error('Failed:', err.message);
|
||||||
|
if (err.type) console.error('Type:', err.type);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "simple-ai-provider",
|
"name": "simple-ai-provider",
|
||||||
"version": "2.0.1",
|
"version": "2.1.0",
|
||||||
"description": "A simple and extensible AI provider package for easy integration of multiple AI services",
|
"description": "A simple and extensible AI provider package for easy integration of multiple AI services",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/index.mjs",
|
"module": "dist/index.mjs",
|
||||||
@@ -21,8 +21,8 @@
|
|||||||
"build": "bun run build:clean && bun run build:types && bun run build:esm && bun run build:cjs",
|
"build": "bun run build:clean && bun run build:types && bun run build:esm && bun run build:cjs",
|
||||||
"build:clean": "rm -rf dist",
|
"build:clean": "rm -rf dist",
|
||||||
"build:types": "tsc --project tsconfig.build.json",
|
"build:types": "tsc --project tsconfig.build.json",
|
||||||
"build:esm": "bun build src/index.ts --outfile dist/index.mjs --format esm",
|
"build:esm": "bun build src/index.ts --outfile dist/index.mjs --format esm --target node",
|
||||||
"build:cjs": "bun build src/index.ts --outfile dist/index.js --format cjs",
|
"build:cjs": "bun build src/index.ts --outfile dist/index.js --format cjs --target node",
|
||||||
"dev": "bun run src/index.ts",
|
"dev": "bun run src/index.ts",
|
||||||
"test": "bun test",
|
"test": "bun test",
|
||||||
"prepublishOnly": "bun run build"
|
"prepublishOnly": "bun run build"
|
||||||
@@ -52,6 +52,7 @@
|
|||||||
"url": "https://gitea.jleibl.net/jleibl/simple-ai-provider.git"
|
"url": "https://gitea.jleibl.net/jleibl/simple-ai-provider.git"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@anthropic-ai/claude-agent-sdk": "^0.3.146",
|
||||||
"@anthropic-ai/sdk": "^0.97.0",
|
"@anthropic-ai/sdk": "^0.97.0",
|
||||||
"@google/genai": "^2.5.0",
|
"@google/genai": "^2.5.0",
|
||||||
"openai": "^6.0.0"
|
"openai": "^6.0.0"
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ export const DEFAULT_MODELS = {
|
|||||||
claude: 'claude-3-5-sonnet-20241022',
|
claude: 'claude-3-5-sonnet-20241022',
|
||||||
openai: 'gpt-4o',
|
openai: 'gpt-4o',
|
||||||
gemini: 'gemini-2.5-flash',
|
gemini: 'gemini-2.5-flash',
|
||||||
openwebui: 'llama3.1:latest'
|
openwebui: 'llama3.1:latest',
|
||||||
|
'claude-code': 'sonnet'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const DEFAULT_OPENWEBUI_BASE_URL = 'http://localhost:3000';
|
export const DEFAULT_OPENWEBUI_BASE_URL = 'http://localhost:3000';
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export { ClaudeProvider, type ClaudeConfig } from './providers/claude.js';
|
|||||||
export { OpenAIProvider, type OpenAIConfig } from './providers/openai.js';
|
export { OpenAIProvider, type OpenAIConfig } from './providers/openai.js';
|
||||||
export { GeminiProvider, type GeminiConfig } from './providers/gemini.js';
|
export { GeminiProvider, type GeminiConfig } from './providers/gemini.js';
|
||||||
export { OpenWebUIProvider, type OpenWebUIConfig } from './providers/openwebui.js';
|
export { OpenWebUIProvider, type OpenWebUIConfig } from './providers/openwebui.js';
|
||||||
|
export { ClaudeCodeProvider, type ClaudeCodeConfig } from './providers/claude-code.js';
|
||||||
|
|
||||||
// Factory utilities
|
// Factory utilities
|
||||||
export {
|
export {
|
||||||
@@ -46,6 +47,7 @@ export {
|
|||||||
createOpenAIProvider,
|
createOpenAIProvider,
|
||||||
createGeminiProvider,
|
createGeminiProvider,
|
||||||
createOpenWebUIProvider,
|
createOpenWebUIProvider,
|
||||||
|
createClaudeCodeProvider,
|
||||||
type ProviderType,
|
type ProviderType,
|
||||||
PROVIDER_REGISTRY
|
PROVIDER_REGISTRY
|
||||||
} from './utils/factory.js';
|
} from './utils/factory.js';
|
||||||
@@ -53,9 +55,9 @@ export {
|
|||||||
/**
|
/**
|
||||||
* List of all supported providers
|
* List of all supported providers
|
||||||
*/
|
*/
|
||||||
export const SUPPORTED_PROVIDERS = ['claude', 'openai', 'gemini', 'openwebui'] as const;
|
export const SUPPORTED_PROVIDERS = ['claude', 'openai', 'gemini', 'openwebui', 'claude-code'] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Package version
|
* Package version
|
||||||
*/
|
*/
|
||||||
export const VERSION = '2.0.1';
|
export const VERSION = '2.1.0';
|
||||||
|
|||||||
403
src/providers/claude-code.ts
Normal file
403
src/providers/claude-code.ts
Normal file
@@ -0,0 +1,403 @@
|
|||||||
|
/**
|
||||||
|
* Claude Code Provider
|
||||||
|
*
|
||||||
|
* Wraps `@anthropic-ai/claude-agent-sdk` so consumers can use a local
|
||||||
|
* Claude Code installation — including Claude Pro/Max subscription
|
||||||
|
* accounts authenticated via `claude login` — through the same
|
||||||
|
* BaseAIProvider interface as the other providers.
|
||||||
|
*
|
||||||
|
* Tradeoffs vs the direct Anthropic API provider:
|
||||||
|
* - Authenticates via the local CLI, so subscription users (no API key)
|
||||||
|
* can use it.
|
||||||
|
* - Requires `claude` to be installed and logged in on the host.
|
||||||
|
* - Higher latency (shells out to a CLI process per request).
|
||||||
|
* - Designed for agent workflows, but used here in single-turn mode
|
||||||
|
* (maxTurns: 1, no tools) for plain text completion.
|
||||||
|
*
|
||||||
|
* @see https://docs.anthropic.com/claude/docs/claude-code-sdk
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||||
|
import type {
|
||||||
|
AIMessage,
|
||||||
|
AIProviderConfig,
|
||||||
|
CompletionChunk,
|
||||||
|
CompletionParams,
|
||||||
|
CompletionResponse,
|
||||||
|
ProviderInfo
|
||||||
|
} from '../types/index.js';
|
||||||
|
import { BaseAIProvider } from './base.js';
|
||||||
|
import { AIProviderError, AIErrorType } from '../types/index.js';
|
||||||
|
import { DEFAULT_MODELS } from '../constants.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for the Claude Code provider.
|
||||||
|
*
|
||||||
|
* Authentication options, in priority order:
|
||||||
|
*
|
||||||
|
* 1. `oauthToken` — long-lived subscription token generated by
|
||||||
|
* `claude setup-token` (recommended for Pro/Max subscribers using
|
||||||
|
* the SDK programmatically). Billed against the subscription.
|
||||||
|
* 2. `apiKey` — standard Anthropic console API key. Billed per-token.
|
||||||
|
* 3. Environment fallback — the SDK reads `CLAUDE_CODE_OAUTH_TOKEN` or
|
||||||
|
* `ANTHROPIC_API_KEY` from the environment if neither field is set.
|
||||||
|
*
|
||||||
|
* Note: `claude login` alone is not sufficient for SDK use because
|
||||||
|
* Anthropic gates non-interactive (`claude -p`) invocations behind a
|
||||||
|
* separate token. Run `claude setup-token` once to mint one.
|
||||||
|
*/
|
||||||
|
export interface ClaudeCodeConfig extends Omit<AIProviderConfig, 'apiKey'> {
|
||||||
|
/**
|
||||||
|
* Long-lived OAuth token from `claude setup-token`. This is the
|
||||||
|
* supported path for Claude Pro/Max subscribers to use the SDK
|
||||||
|
* programmatically — billed against the subscription instead of
|
||||||
|
* per-token.
|
||||||
|
*/
|
||||||
|
oauthToken?: string;
|
||||||
|
|
||||||
|
/** Standard Anthropic console API key. Mutually compatible with oauthToken (oauthToken takes precedence). */
|
||||||
|
apiKey?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default model. Accepts SDK aliases (`'sonnet'`, `'opus'`, `'haiku'`,
|
||||||
|
* `'inherit'`) or any full Claude model ID.
|
||||||
|
* @default 'sonnet'
|
||||||
|
*/
|
||||||
|
defaultModel?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Working directory for the Claude Code session.
|
||||||
|
* @default process.cwd()
|
||||||
|
*/
|
||||||
|
cwd?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool names the model is allowed to call. Defaults to none, which
|
||||||
|
* makes the provider behave as a pure text completion endpoint.
|
||||||
|
*/
|
||||||
|
allowedTools?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum agent turns. Defaults to 1 for plain completion; raise this
|
||||||
|
* if you also pass `allowedTools` and want tool-use loops.
|
||||||
|
* @default 1
|
||||||
|
*/
|
||||||
|
maxTurns?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ClaudeCodeProvider extends BaseAIProvider {
|
||||||
|
private readonly defaultModel: string;
|
||||||
|
private readonly cwd: string | undefined;
|
||||||
|
private readonly allowedTools: string[];
|
||||||
|
private readonly maxTurns: number;
|
||||||
|
private readonly oauthToken: string | undefined;
|
||||||
|
|
||||||
|
constructor(config: ClaudeCodeConfig) {
|
||||||
|
super(config as AIProviderConfig);
|
||||||
|
|
||||||
|
this.defaultModel = config.defaultModel || DEFAULT_MODELS['claude-code'];
|
||||||
|
this.cwd = config.cwd;
|
||||||
|
this.allowedTools = config.allowedTools ?? [];
|
||||||
|
this.maxTurns = config.maxTurns ?? 1;
|
||||||
|
this.oauthToken = config.oauthToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* apiKey is optional for this provider — the SDK reads
|
||||||
|
* ANTHROPIC_API_KEY from the environment or falls back to the local
|
||||||
|
* Claude Code credentials. We override the base validator to skip the
|
||||||
|
* non-empty apiKey requirement.
|
||||||
|
*/
|
||||||
|
protected override validateAndNormalizeConfig(config: AIProviderConfig): AIProviderConfig {
|
||||||
|
if (!config) {
|
||||||
|
throw new AIProviderError(
|
||||||
|
'Configuration object is required',
|
||||||
|
AIErrorType.INVALID_REQUEST
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.apiKey !== undefined && typeof config.apiKey !== 'string') {
|
||||||
|
throw new AIProviderError(
|
||||||
|
'API key, when provided, must be a string',
|
||||||
|
AIErrorType.INVALID_REQUEST
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const oauthToken = (config as ClaudeCodeConfig).oauthToken;
|
||||||
|
if (oauthToken !== undefined && typeof oauthToken !== 'string') {
|
||||||
|
throw new AIProviderError(
|
||||||
|
'oauthToken, when provided, must be a string',
|
||||||
|
AIErrorType.INVALID_REQUEST
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
apiKey: config.apiKey ?? '',
|
||||||
|
timeout: config.timeout,
|
||||||
|
maxRetries: config.maxRetries
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async doInitialize(): Promise<void> {
|
||||||
|
// The SDK lazy-spawns the CLI per query; no connection probe is
|
||||||
|
// necessary (and a probe would cost a real billed turn). We defer
|
||||||
|
// auth/model errors to the first complete()/stream() call.
|
||||||
|
if (this.oauthToken) {
|
||||||
|
process.env.CLAUDE_CODE_OAUTH_TOKEN = this.oauthToken;
|
||||||
|
}
|
||||||
|
if (this.config.apiKey) {
|
||||||
|
process.env.ANTHROPIC_API_KEY = this.config.apiKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async doComplete(params: CompletionParams): Promise<CompletionResponse<string>> {
|
||||||
|
const { systemPrompt, prompt } = this.buildPrompt(params.messages);
|
||||||
|
const model = params.model || this.defaultModel;
|
||||||
|
|
||||||
|
let collectedText = '';
|
||||||
|
let sessionId = '';
|
||||||
|
let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
||||||
|
|
||||||
|
for await (const message of query({
|
||||||
|
prompt,
|
||||||
|
options: this.buildOptions(model, systemPrompt)
|
||||||
|
})) {
|
||||||
|
if (message.type === 'system' && message.subtype === 'init') {
|
||||||
|
sessionId = message.session_id;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === 'assistant') {
|
||||||
|
if (message.error) {
|
||||||
|
throw this.errorForSdkAssistantError(message.error);
|
||||||
|
}
|
||||||
|
collectedText += extractText(message.message.content);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === 'result') {
|
||||||
|
usage = {
|
||||||
|
promptTokens: message.usage?.input_tokens ?? 0,
|
||||||
|
completionTokens: message.usage?.output_tokens ?? 0,
|
||||||
|
totalTokens: (message.usage?.input_tokens ?? 0) + (message.usage?.output_tokens ?? 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (message.subtype !== 'success') {
|
||||||
|
throw new AIProviderError(
|
||||||
|
`Claude Code session ended without a successful result: ${message.subtype}`,
|
||||||
|
AIErrorType.UNKNOWN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: collectedText || message.result,
|
||||||
|
model,
|
||||||
|
usage,
|
||||||
|
id: sessionId || message.uuid,
|
||||||
|
metadata: {
|
||||||
|
costUsd: message.total_cost_usd,
|
||||||
|
durationMs: message.duration_ms,
|
||||||
|
numTurns: message.num_turns,
|
||||||
|
stopReason: message.stop_reason
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new AIProviderError(
|
||||||
|
'Claude Code stream ended without a result message',
|
||||||
|
AIErrorType.UNKNOWN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async *doStream<T = any>(
|
||||||
|
params: CompletionParams<T>
|
||||||
|
): AsyncIterable<CompletionChunk> {
|
||||||
|
const { systemPrompt, prompt } = this.buildPrompt(params.messages);
|
||||||
|
const model = params.model || this.defaultModel;
|
||||||
|
|
||||||
|
let sessionId = '';
|
||||||
|
let yieldedText = '';
|
||||||
|
|
||||||
|
for await (const message of query({
|
||||||
|
prompt,
|
||||||
|
options: this.buildOptions(model, systemPrompt)
|
||||||
|
})) {
|
||||||
|
if (message.type === 'system' && message.subtype === 'init') {
|
||||||
|
sessionId = message.session_id;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === 'assistant') {
|
||||||
|
if (message.error) {
|
||||||
|
throw this.errorForSdkAssistantError(message.error);
|
||||||
|
}
|
||||||
|
const fullText = extractText(message.message.content);
|
||||||
|
const delta = fullText.slice(yieldedText.length);
|
||||||
|
if (delta) {
|
||||||
|
yield { content: delta, isComplete: false, id: sessionId };
|
||||||
|
yieldedText = fullText;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === 'result') {
|
||||||
|
const usage = {
|
||||||
|
promptTokens: message.usage?.input_tokens ?? 0,
|
||||||
|
completionTokens: message.usage?.output_tokens ?? 0,
|
||||||
|
totalTokens: (message.usage?.input_tokens ?? 0) + (message.usage?.output_tokens ?? 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (message.subtype !== 'success') {
|
||||||
|
throw new AIProviderError(
|
||||||
|
`Claude Code session ended without a successful result: ${message.subtype}`,
|
||||||
|
AIErrorType.UNKNOWN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
yield { content: '', isComplete: true, id: sessionId, usage };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getInfo(): ProviderInfo {
|
||||||
|
return {
|
||||||
|
name: 'Claude Code',
|
||||||
|
version: '1.0.0',
|
||||||
|
models: ['sonnet', 'opus', 'haiku', 'inherit'],
|
||||||
|
maxContextLength: 200000,
|
||||||
|
supportsStreaming: true,
|
||||||
|
capabilities: {
|
||||||
|
vision: true,
|
||||||
|
functionCalling: true,
|
||||||
|
systemMessages: true,
|
||||||
|
reasoning: true,
|
||||||
|
codeGeneration: true,
|
||||||
|
subscriptionAuth: true,
|
||||||
|
requiresLocalCli: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override mapProviderError(error: any): AIProviderError | null {
|
||||||
|
if (!error) return null;
|
||||||
|
const message: string = error.message || '';
|
||||||
|
|
||||||
|
if (message.includes('ENOENT') && message.toLowerCase().includes('claude')) {
|
||||||
|
return new AIProviderError(
|
||||||
|
'Claude Code CLI not found. Install it from https://docs.anthropic.com/claude/docs/claude-code and run `claude login` for subscription accounts.',
|
||||||
|
AIErrorType.INVALID_REQUEST,
|
||||||
|
undefined,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.includes('not logged in') || message.includes('authentication')) {
|
||||||
|
return new AIProviderError(
|
||||||
|
'Claude Code is not authenticated. Run `claude login` or set ANTHROPIC_API_KEY in the environment.',
|
||||||
|
AIErrorType.AUTHENTICATION,
|
||||||
|
undefined,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildPrompt(messages: AIMessage[]): { systemPrompt: string | undefined; prompt: string } {
|
||||||
|
let systemPrompt: string | undefined;
|
||||||
|
const turns: AIMessage[] = [];
|
||||||
|
|
||||||
|
for (const msg of messages) {
|
||||||
|
if (msg.role === 'system') {
|
||||||
|
systemPrompt = systemPrompt ? `${systemPrompt}\n\n${msg.content}` : msg.content;
|
||||||
|
} else {
|
||||||
|
turns.push(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (turns.length === 0) {
|
||||||
|
throw new AIProviderError(
|
||||||
|
'At least one user or assistant message is required',
|
||||||
|
AIErrorType.INVALID_REQUEST
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single user turn: pass the content directly. Multi-turn history is
|
||||||
|
// flattened with role labels — the Agent SDK doesn't accept a
|
||||||
|
// messages array in stateless mode, so this is the simplest faithful
|
||||||
|
// representation of the conversation.
|
||||||
|
if (turns.length === 1 && turns[0]!.role === 'user') {
|
||||||
|
return { systemPrompt, prompt: turns[0]!.content };
|
||||||
|
}
|
||||||
|
|
||||||
|
const flattened = turns
|
||||||
|
.map(t => `${t.role === 'assistant' ? 'Assistant' : 'Human'}: ${t.content}`)
|
||||||
|
.join('\n\n');
|
||||||
|
return { systemPrompt, prompt: `${flattened}\n\nAssistant:` };
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildOptions(model: string, systemPrompt: string | undefined) {
|
||||||
|
const options: Record<string, unknown> = {
|
||||||
|
model,
|
||||||
|
maxTurns: this.maxTurns,
|
||||||
|
allowedTools: this.allowedTools
|
||||||
|
};
|
||||||
|
if (systemPrompt) options.systemPrompt = systemPrompt;
|
||||||
|
if (this.cwd) options.cwd = this.cwd;
|
||||||
|
return options as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
private errorForSdkAssistantError(code: string): AIProviderError {
|
||||||
|
const map: Record<string, { type: AIErrorType; message: string }> = {
|
||||||
|
authentication_failed: {
|
||||||
|
type: AIErrorType.AUTHENTICATION,
|
||||||
|
message: 'Claude Code authentication failed. For subscriptions run `claude setup-token` and pass the result as `oauthToken`. Otherwise set ANTHROPIC_API_KEY or pass `apiKey`.'
|
||||||
|
},
|
||||||
|
oauth_org_not_allowed: {
|
||||||
|
type: AIErrorType.AUTHENTICATION,
|
||||||
|
message: 'Your organization is not allowed by the Claude Code OAuth policy.'
|
||||||
|
},
|
||||||
|
billing_error: {
|
||||||
|
type: AIErrorType.AUTHENTICATION,
|
||||||
|
message: 'Billing error. For Claude Pro/Max subscribers using the SDK: run `claude setup-token` and pass the resulting token as `oauthToken` (interactive `claude login` alone is not sufficient for non-interactive SDK calls).'
|
||||||
|
},
|
||||||
|
rate_limit: {
|
||||||
|
type: AIErrorType.RATE_LIMIT,
|
||||||
|
message: 'Rate limit exceeded.'
|
||||||
|
},
|
||||||
|
invalid_request: {
|
||||||
|
type: AIErrorType.INVALID_REQUEST,
|
||||||
|
message: 'Invalid request to Claude Code.'
|
||||||
|
},
|
||||||
|
model_not_found: {
|
||||||
|
type: AIErrorType.MODEL_NOT_FOUND,
|
||||||
|
message: 'Requested model is not available to this Claude Code installation.'
|
||||||
|
},
|
||||||
|
server_error: {
|
||||||
|
type: AIErrorType.NETWORK,
|
||||||
|
message: 'Claude Code server error. Try again shortly.'
|
||||||
|
},
|
||||||
|
max_output_tokens: {
|
||||||
|
type: AIErrorType.INVALID_REQUEST,
|
||||||
|
message: 'Response exceeded the configured maxOutputTokens.'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const entry = map[code] ?? { type: AIErrorType.UNKNOWN, message: `Claude Code error: ${code}` };
|
||||||
|
return new AIProviderError(entry.message, entry.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractText(content: unknown): string {
|
||||||
|
if (!Array.isArray(content)) return '';
|
||||||
|
let out = '';
|
||||||
|
for (const block of content) {
|
||||||
|
if (block && typeof block === 'object' && (block as any).type === 'text') {
|
||||||
|
out += (block as any).text ?? '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
@@ -8,3 +8,4 @@ export { ClaudeProvider, type ClaudeConfig } from './claude.js';
|
|||||||
export { OpenAIProvider, type OpenAIConfig } from './openai.js';
|
export { OpenAIProvider, type OpenAIConfig } from './openai.js';
|
||||||
export { GeminiProvider, type GeminiConfig } from './gemini.js';
|
export { GeminiProvider, type GeminiConfig } from './gemini.js';
|
||||||
export { OpenWebUIProvider, type OpenWebUIConfig } from './openwebui.js';
|
export { OpenWebUIProvider, type OpenWebUIConfig } from './openwebui.js';
|
||||||
|
export { ClaudeCodeProvider, type ClaudeCodeConfig } from './claude-code.js';
|
||||||
|
|||||||
@@ -6,13 +6,15 @@ import { ClaudeProvider, type ClaudeConfig } from '../providers/claude.js';
|
|||||||
import { OpenAIProvider, type OpenAIConfig } from '../providers/openai.js';
|
import { OpenAIProvider, type OpenAIConfig } from '../providers/openai.js';
|
||||||
import { GeminiProvider, type GeminiConfig } from '../providers/gemini.js';
|
import { GeminiProvider, type GeminiConfig } from '../providers/gemini.js';
|
||||||
import { OpenWebUIProvider, type OpenWebUIConfig } from '../providers/openwebui.js';
|
import { OpenWebUIProvider, type OpenWebUIConfig } from '../providers/openwebui.js';
|
||||||
|
import { ClaudeCodeProvider, type ClaudeCodeConfig } from '../providers/claude-code.js';
|
||||||
import { BaseAIProvider } from '../providers/base.js';
|
import { BaseAIProvider } from '../providers/base.js';
|
||||||
|
|
||||||
export const PROVIDER_REGISTRY = {
|
export const PROVIDER_REGISTRY = {
|
||||||
claude: ClaudeProvider,
|
claude: ClaudeProvider,
|
||||||
openai: OpenAIProvider,
|
openai: OpenAIProvider,
|
||||||
gemini: GeminiProvider,
|
gemini: GeminiProvider,
|
||||||
openwebui: OpenWebUIProvider
|
openwebui: OpenWebUIProvider,
|
||||||
|
'claude-code': ClaudeCodeProvider
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type ProviderType = keyof typeof PROVIDER_REGISTRY;
|
export type ProviderType = keyof typeof PROVIDER_REGISTRY;
|
||||||
@@ -22,6 +24,7 @@ export interface ProviderConfigMap {
|
|||||||
openai: OpenAIConfig;
|
openai: OpenAIConfig;
|
||||||
gemini: GeminiConfig;
|
gemini: GeminiConfig;
|
||||||
openwebui: OpenWebUIConfig;
|
openwebui: OpenWebUIConfig;
|
||||||
|
'claude-code': ClaudeCodeConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createProvider<T extends ProviderType>(
|
export function createProvider<T extends ProviderType>(
|
||||||
@@ -59,3 +62,9 @@ export function createGeminiProvider(
|
|||||||
export function createOpenWebUIProvider(config: OpenWebUIConfig): OpenWebUIProvider {
|
export function createOpenWebUIProvider(config: OpenWebUIConfig): OpenWebUIProvider {
|
||||||
return new OpenWebUIProvider(config);
|
return new OpenWebUIProvider(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createClaudeCodeProvider(
|
||||||
|
options: Partial<ClaudeCodeConfig> = {}
|
||||||
|
): ClaudeCodeProvider {
|
||||||
|
return new ClaudeCodeProvider(options);
|
||||||
|
}
|
||||||
|
|||||||
178
tests/claude-code.test.ts
Normal file
178
tests/claude-code.test.ts
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
/**
|
||||||
|
* Tests for ClaudeCodeProvider
|
||||||
|
*
|
||||||
|
* The provider shells out to the local Claude Code CLI via
|
||||||
|
* @anthropic-ai/claude-agent-sdk. Tests cover the public surface and the
|
||||||
|
* provider-specific hooks; full integration is not exercised because it
|
||||||
|
* would spawn a real CLI process.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeEach } from 'bun:test';
|
||||||
|
import { ClaudeCodeProvider } from '../src/providers/claude-code.js';
|
||||||
|
import { AIProviderError, AIErrorType } from '../src/types/index.js';
|
||||||
|
|
||||||
|
describe('ClaudeCodeProvider', () => {
|
||||||
|
let provider: ClaudeCodeProvider;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
provider = new ClaudeCodeProvider({ apiKey: 'test-key' });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Constructor', () => {
|
||||||
|
it('should accept empty config (subscription mode)', () => {
|
||||||
|
const p = new ClaudeCodeProvider({});
|
||||||
|
expect(p).toBeInstanceOf(ClaudeCodeProvider);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept apiKey override', () => {
|
||||||
|
const p = new ClaudeCodeProvider({ apiKey: 'sk-...' });
|
||||||
|
expect(p).toBeInstanceOf(ClaudeCodeProvider);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept custom defaults', () => {
|
||||||
|
const p = new ClaudeCodeProvider({
|
||||||
|
defaultModel: 'opus',
|
||||||
|
maxTurns: 5,
|
||||||
|
allowedTools: ['Read', 'Bash'],
|
||||||
|
cwd: '/tmp/agent'
|
||||||
|
});
|
||||||
|
expect(p).toBeInstanceOf(ClaudeCodeProvider);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject non-string apiKey', () => {
|
||||||
|
expect(() => new ClaudeCodeProvider({ apiKey: 123 as any }))
|
||||||
|
.toThrow(AIProviderError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getInfo', () => {
|
||||||
|
it('should advertise subscriptionAuth capability', () => {
|
||||||
|
const info = provider.getInfo();
|
||||||
|
|
||||||
|
expect(info.name).toBe('Claude Code');
|
||||||
|
expect(info.supportsStreaming).toBe(true);
|
||||||
|
expect(info.capabilities).toMatchObject({
|
||||||
|
subscriptionAuth: true,
|
||||||
|
requiresLocalCli: true,
|
||||||
|
systemMessages: true
|
||||||
|
});
|
||||||
|
expect(info.models).toContain('sonnet');
|
||||||
|
expect(info.models).toContain('opus');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Error mapping', () => {
|
||||||
|
const mapError = (err: any) => (provider as any).mapProviderError(err);
|
||||||
|
|
||||||
|
it('should detect missing CLI', () => {
|
||||||
|
const err = new Error('spawn claude ENOENT');
|
||||||
|
|
||||||
|
const mapped = mapError(err);
|
||||||
|
|
||||||
|
expect(mapped).toBeInstanceOf(AIProviderError);
|
||||||
|
expect(mapped.type).toBe(AIErrorType.INVALID_REQUEST);
|
||||||
|
expect(mapped.message).toContain('CLI');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect missing authentication', () => {
|
||||||
|
const err = new Error('not logged in');
|
||||||
|
|
||||||
|
const mapped = mapError(err);
|
||||||
|
|
||||||
|
expect(mapped).toBeInstanceOf(AIProviderError);
|
||||||
|
expect(mapped.type).toBe(AIErrorType.AUTHENTICATION);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null for unrelated errors', () => {
|
||||||
|
expect(mapError(new Error('something else'))).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('SDK assistant error mapping', () => {
|
||||||
|
const mapSdkError = (code: string) => (provider as any).errorForSdkAssistantError(code);
|
||||||
|
|
||||||
|
it('should map authentication_failed', () => {
|
||||||
|
const err = mapSdkError('authentication_failed');
|
||||||
|
expect(err.type).toBe(AIErrorType.AUTHENTICATION);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map rate_limit', () => {
|
||||||
|
const err = mapSdkError('rate_limit');
|
||||||
|
expect(err.type).toBe(AIErrorType.RATE_LIMIT);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map model_not_found', () => {
|
||||||
|
const err = mapSdkError('model_not_found');
|
||||||
|
expect(err.type).toBe(AIErrorType.MODEL_NOT_FOUND);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map server_error to NETWORK', () => {
|
||||||
|
const err = mapSdkError('server_error');
|
||||||
|
expect(err.type).toBe(AIErrorType.NETWORK);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fall back to UNKNOWN for unrecognized codes', () => {
|
||||||
|
const err = mapSdkError('weird_new_code');
|
||||||
|
expect(err.type).toBe(AIErrorType.UNKNOWN);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Prompt building', () => {
|
||||||
|
const build = (messages: any[]) => (provider as any).buildPrompt(messages);
|
||||||
|
|
||||||
|
it('should pass single user turn through unchanged', () => {
|
||||||
|
const result = build([{ role: 'user', content: 'Hello' }]);
|
||||||
|
|
||||||
|
expect(result.prompt).toBe('Hello');
|
||||||
|
expect(result.systemPrompt).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should combine multiple system messages', () => {
|
||||||
|
const result = build([
|
||||||
|
{ role: 'system', content: 'Be brief' },
|
||||||
|
{ role: 'system', content: 'Use British spelling' },
|
||||||
|
{ role: 'user', content: 'Hi' }
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(result.systemPrompt).toBe('Be brief\n\nUse British spelling');
|
||||||
|
expect(result.prompt).toBe('Hi');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should flatten multi-turn conversation with role labels', () => {
|
||||||
|
const result = build([
|
||||||
|
{ role: 'user', content: 'What is 2+2?' },
|
||||||
|
{ role: 'assistant', content: '4' },
|
||||||
|
{ role: 'user', content: 'Why?' }
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(result.prompt).toContain('Human: What is 2+2?');
|
||||||
|
expect(result.prompt).toContain('Assistant: 4');
|
||||||
|
expect(result.prompt).toContain('Human: Why?');
|
||||||
|
expect(result.prompt.endsWith('Assistant:')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject empty turn list', () => {
|
||||||
|
expect(() => build([{ role: 'system', content: 'only system' }]))
|
||||||
|
.toThrow(AIProviderError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Validation', () => {
|
||||||
|
it('should require initialization before complete()', async () => {
|
||||||
|
await expect(
|
||||||
|
provider.complete({ messages: [{ role: 'user', content: 'test' }] })
|
||||||
|
).rejects.toThrow('Provider must be initialized before use');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate temperature range when initialized', async () => {
|
||||||
|
(provider as any).initialized = true;
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
provider.complete({
|
||||||
|
messages: [{ role: 'user', content: 'test' }],
|
||||||
|
temperature: 1.5
|
||||||
|
})
|
||||||
|
).rejects.toThrow(/temperature must be at most 1/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -51,7 +51,7 @@ describe('ClaudeProvider', () => {
|
|||||||
messages: [{ role: 'user', content: 'test' }],
|
messages: [{ role: 'user', content: 'test' }],
|
||||||
temperature: 1.5
|
temperature: 1.5
|
||||||
})
|
})
|
||||||
).rejects.toThrow('Temperature must be a number between 0.0 and 1.0');
|
).rejects.toThrow(/temperature must be at most 1/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should validate message format', async () => {
|
it('should validate message format', async () => {
|
||||||
@@ -79,17 +79,18 @@ describe('ClaudeProvider', () => {
|
|||||||
const error = new Error('Unauthorized');
|
const error = new Error('Unauthorized');
|
||||||
(error as any).status = 401;
|
(error as any).status = 401;
|
||||||
|
|
||||||
const providerError = (provider as any).handleAnthropicError(error);
|
const providerError = (provider as any).normalizeError(error);
|
||||||
|
|
||||||
expect(providerError).toBeInstanceOf(AIProviderError);
|
expect(providerError).toBeInstanceOf(AIProviderError);
|
||||||
expect(providerError.type).toBe(AIErrorType.AUTHENTICATION);
|
expect(providerError.type).toBe(AIErrorType.AUTHENTICATION);
|
||||||
|
expect(providerError.statusCode).toBe(401);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle rate limit errors', () => {
|
it('should handle rate limit errors', () => {
|
||||||
const error = new Error('Rate limited');
|
const error = new Error('Rate limited');
|
||||||
(error as any).status = 429;
|
(error as any).status = 429;
|
||||||
|
|
||||||
const providerError = (provider as any).handleAnthropicError(error);
|
const providerError = (provider as any).normalizeError(error);
|
||||||
|
|
||||||
expect(providerError).toBeInstanceOf(AIProviderError);
|
expect(providerError).toBeInstanceOf(AIProviderError);
|
||||||
expect(providerError.type).toBe(AIErrorType.RATE_LIMIT);
|
expect(providerError.type).toBe(AIErrorType.RATE_LIMIT);
|
||||||
@@ -99,11 +100,21 @@ describe('ClaudeProvider', () => {
|
|||||||
const error = new Error('Model not found');
|
const error = new Error('Model not found');
|
||||||
(error as any).status = 404;
|
(error as any).status = 404;
|
||||||
|
|
||||||
const providerError = (provider as any).handleAnthropicError(error);
|
const providerError = (provider as any).normalizeError(error);
|
||||||
|
|
||||||
expect(providerError).toBeInstanceOf(AIProviderError);
|
expect(providerError).toBeInstanceOf(AIProviderError);
|
||||||
expect(providerError.type).toBe(AIErrorType.MODEL_NOT_FOUND);
|
expect(providerError.type).toBe(AIErrorType.MODEL_NOT_FOUND);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should route 400 with max_tokens to mapProviderError', () => {
|
||||||
|
const error = new Error('max_tokens too large');
|
||||||
|
(error as any).status = 400;
|
||||||
|
|
||||||
|
const providerError = (provider as any).normalizeError(error);
|
||||||
|
|
||||||
|
expect(providerError.type).toBe(AIErrorType.INVALID_REQUEST);
|
||||||
|
expect(providerError.message).toMatch(/Max tokens/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('message conversion', () => {
|
describe('message conversion', () => {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ describe('GeminiProvider', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
provider = new GeminiProvider({
|
provider = new GeminiProvider({
|
||||||
apiKey: 'test-api-key',
|
apiKey: 'test-api-key',
|
||||||
defaultModel: 'gemini-1.5-flash'
|
defaultModel: 'gemini-2.5-flash'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -54,11 +54,11 @@ describe('GeminiProvider', () => {
|
|||||||
const info = provider.getInfo();
|
const info = provider.getInfo();
|
||||||
|
|
||||||
expect(info.name).toBe('Gemini');
|
expect(info.name).toBe('Gemini');
|
||||||
expect(info.version).toBe('1.0.0');
|
expect(info.version).toBe('2.0.0');
|
||||||
expect(info.supportsStreaming).toBe(true);
|
expect(info.supportsStreaming).toBe(true);
|
||||||
expect(info.models).toContain('gemini-1.5-flash');
|
expect(info.models).toContain('gemini-2.5-flash');
|
||||||
|
expect(info.models).toContain('gemini-2.5-pro');
|
||||||
expect(info.models).toContain('gemini-1.5-pro');
|
expect(info.models).toContain('gemini-1.5-pro');
|
||||||
expect(info.models).toContain('gemini-1.0-pro');
|
|
||||||
expect(info.maxContextLength).toBe(1048576);
|
expect(info.maxContextLength).toBe(1048576);
|
||||||
expect(info.capabilities).toHaveProperty('vision', true);
|
expect(info.capabilities).toHaveProperty('vision', true);
|
||||||
expect(info.capabilities).toHaveProperty('functionCalling', true);
|
expect(info.capabilities).toHaveProperty('functionCalling', true);
|
||||||
@@ -79,7 +79,7 @@ describe('GeminiProvider', () => {
|
|||||||
messages: [{ role: 'user', content: 'test' }],
|
messages: [{ role: 'user', content: 'test' }],
|
||||||
temperature: 1.5
|
temperature: 1.5
|
||||||
})
|
})
|
||||||
).rejects.toThrow('Temperature must be a number between 0.0 and 1.0');
|
).rejects.toThrow(/temperature must be at most 1/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should validate top_p range', async () => {
|
it('should validate top_p range', async () => {
|
||||||
@@ -92,7 +92,7 @@ describe('GeminiProvider', () => {
|
|||||||
messages: [{ role: 'user', content: 'test' }],
|
messages: [{ role: 'user', content: 'test' }],
|
||||||
topP: 1.5
|
topP: 1.5
|
||||||
})
|
})
|
||||||
).rejects.toThrow('Top-p must be a number between 0.0 (exclusive) and 1.0 (inclusive)');
|
).rejects.toThrow(/topP must be at most 1/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should validate message format', async () => {
|
it('should validate message format', async () => {
|
||||||
@@ -129,10 +129,12 @@ describe('GeminiProvider', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('error handling', () => {
|
describe('error handling', () => {
|
||||||
|
const mapError = (err: any) => (provider as any).normalizeError(err);
|
||||||
|
|
||||||
it('should handle authentication errors', () => {
|
it('should handle authentication errors', () => {
|
||||||
const error = new Error('API key invalid');
|
const error = new Error('API key invalid');
|
||||||
|
|
||||||
const providerError = (provider as any).handleGeminiError(error);
|
const providerError = mapError(error);
|
||||||
|
|
||||||
expect(providerError).toBeInstanceOf(AIProviderError);
|
expect(providerError).toBeInstanceOf(AIProviderError);
|
||||||
expect(providerError.type).toBe(AIErrorType.AUTHENTICATION);
|
expect(providerError.type).toBe(AIErrorType.AUTHENTICATION);
|
||||||
@@ -142,7 +144,7 @@ describe('GeminiProvider', () => {
|
|||||||
it('should handle rate limit errors', () => {
|
it('should handle rate limit errors', () => {
|
||||||
const error = new Error('quota exceeded');
|
const error = new Error('quota exceeded');
|
||||||
|
|
||||||
const providerError = (provider as any).handleGeminiError(error);
|
const providerError = mapError(error);
|
||||||
|
|
||||||
expect(providerError).toBeInstanceOf(AIProviderError);
|
expect(providerError).toBeInstanceOf(AIProviderError);
|
||||||
expect(providerError.type).toBe(AIErrorType.RATE_LIMIT);
|
expect(providerError.type).toBe(AIErrorType.RATE_LIMIT);
|
||||||
@@ -152,7 +154,7 @@ describe('GeminiProvider', () => {
|
|||||||
it('should handle model not found errors', () => {
|
it('should handle model not found errors', () => {
|
||||||
const error = new Error('model not found');
|
const error = new Error('model not found');
|
||||||
|
|
||||||
const providerError = (provider as any).handleGeminiError(error);
|
const providerError = mapError(error);
|
||||||
|
|
||||||
expect(providerError).toBeInstanceOf(AIProviderError);
|
expect(providerError).toBeInstanceOf(AIProviderError);
|
||||||
expect(providerError.type).toBe(AIErrorType.MODEL_NOT_FOUND);
|
expect(providerError.type).toBe(AIErrorType.MODEL_NOT_FOUND);
|
||||||
@@ -162,7 +164,7 @@ describe('GeminiProvider', () => {
|
|||||||
it('should handle invalid request errors', () => {
|
it('should handle invalid request errors', () => {
|
||||||
const error = new Error('invalid length request parameters');
|
const error = new Error('invalid length request parameters');
|
||||||
|
|
||||||
const providerError = (provider as any).handleGeminiError(error);
|
const providerError = mapError(error);
|
||||||
|
|
||||||
expect(providerError).toBeInstanceOf(AIProviderError);
|
expect(providerError).toBeInstanceOf(AIProviderError);
|
||||||
expect(providerError.type).toBe(AIErrorType.INVALID_REQUEST);
|
expect(providerError.type).toBe(AIErrorType.INVALID_REQUEST);
|
||||||
@@ -171,7 +173,7 @@ describe('GeminiProvider', () => {
|
|||||||
it('should handle network errors', () => {
|
it('should handle network errors', () => {
|
||||||
const error = new Error('network connection failed');
|
const error = new Error('network connection failed');
|
||||||
|
|
||||||
const providerError = (provider as any).handleGeminiError(error);
|
const providerError = mapError(error);
|
||||||
|
|
||||||
expect(providerError).toBeInstanceOf(AIProviderError);
|
expect(providerError).toBeInstanceOf(AIProviderError);
|
||||||
expect(providerError.type).toBe(AIErrorType.NETWORK);
|
expect(providerError.type).toBe(AIErrorType.NETWORK);
|
||||||
@@ -180,7 +182,7 @@ describe('GeminiProvider', () => {
|
|||||||
it('should handle timeout errors', () => {
|
it('should handle timeout errors', () => {
|
||||||
const error = new Error('request timeout');
|
const error = new Error('request timeout');
|
||||||
|
|
||||||
const providerError = (provider as any).handleGeminiError(error);
|
const providerError = mapError(error);
|
||||||
|
|
||||||
expect(providerError).toBeInstanceOf(AIProviderError);
|
expect(providerError).toBeInstanceOf(AIProviderError);
|
||||||
expect(providerError.type).toBe(AIErrorType.TIMEOUT);
|
expect(providerError.type).toBe(AIErrorType.TIMEOUT);
|
||||||
@@ -189,7 +191,7 @@ describe('GeminiProvider', () => {
|
|||||||
it('should handle unknown errors', () => {
|
it('should handle unknown errors', () => {
|
||||||
const error = new Error('Unknown error');
|
const error = new Error('Unknown error');
|
||||||
|
|
||||||
const providerError = (provider as any).handleGeminiError(error);
|
const providerError = mapError(error);
|
||||||
|
|
||||||
expect(providerError).toBeInstanceOf(AIProviderError);
|
expect(providerError).toBeInstanceOf(AIProviderError);
|
||||||
expect(providerError.type).toBe(AIErrorType.UNKNOWN);
|
expect(providerError.type).toBe(AIErrorType.UNKNOWN);
|
||||||
@@ -266,7 +268,7 @@ describe('GeminiProvider', () => {
|
|||||||
stopSequences: ['STOP', 'END']
|
stopSequences: ['STOP', 'END']
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = (provider as any).buildGenerationConfig(params);
|
const result = (provider as any).buildConfig(params);
|
||||||
|
|
||||||
expect(result.temperature).toBe(0.8);
|
expect(result.temperature).toBe(0.8);
|
||||||
expect(result.topP).toBe(0.9);
|
expect(result.topP).toBe(0.9);
|
||||||
@@ -279,20 +281,29 @@ describe('GeminiProvider', () => {
|
|||||||
messages: [{ role: 'user' as const, content: 'test' }]
|
messages: [{ role: 'user' as const, content: 'test' }]
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = (provider as any).buildGenerationConfig(params);
|
const result = (provider as any).buildConfig(params);
|
||||||
|
|
||||||
expect(result.temperature).toBe(0.7);
|
expect(result.temperature).toBe(0.7);
|
||||||
expect(result.maxOutputTokens).toBe(1000);
|
expect(result.maxOutputTokens).toBe(1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should attach systemInstruction when passed', () => {
|
||||||
|
const params = { messages: [{ role: 'user' as const, content: 'test' }] };
|
||||||
|
|
||||||
|
const result = (provider as any).buildConfig(params, 'be helpful');
|
||||||
|
|
||||||
|
expect(result.systemInstruction).toBe('be helpful');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('response formatting', () => {
|
describe('response formatting', () => {
|
||||||
|
// The @google/genai SDK exposes `text` as a getter on GenerateContentResponse,
|
||||||
|
// so the mock provides it directly rather than reconstructing the candidate->parts tree.
|
||||||
|
|
||||||
it('should format completion response correctly', () => {
|
it('should format completion response correctly', () => {
|
||||||
const mockResponse = {
|
const mockResponse = {
|
||||||
|
text: 'Hello there!',
|
||||||
candidates: [{
|
candidates: [{
|
||||||
content: {
|
|
||||||
parts: [{ text: 'Hello there!' }]
|
|
||||||
},
|
|
||||||
finishReason: 'STOP',
|
finishReason: 'STOP',
|
||||||
safetyRatings: [],
|
safetyRatings: [],
|
||||||
citationMetadata: null
|
citationMetadata: null
|
||||||
@@ -304,42 +315,19 @@ describe('GeminiProvider', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = (provider as any).formatCompletionResponse(mockResponse, 'gemini-1.5-flash');
|
const result = (provider as any).formatCompletionResponse(mockResponse, 'gemini-2.5-flash');
|
||||||
|
|
||||||
expect(result.content).toBe('Hello there!');
|
expect(result.content).toBe('Hello there!');
|
||||||
expect(result.model).toBe('gemini-1.5-flash');
|
expect(result.model).toBe('gemini-2.5-flash');
|
||||||
expect(result.usage.promptTokens).toBe(10);
|
expect(result.usage.promptTokens).toBe(10);
|
||||||
expect(result.usage.completionTokens).toBe(20);
|
expect(result.usage.completionTokens).toBe(20);
|
||||||
expect(result.usage.totalTokens).toBe(30);
|
expect(result.usage.totalTokens).toBe(30);
|
||||||
expect(result.metadata.finishReason).toBe('STOP');
|
expect(result.metadata.finishReason).toBe('STOP');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle multiple text parts', () => {
|
|
||||||
const mockResponse = {
|
|
||||||
candidates: [{
|
|
||||||
content: {
|
|
||||||
parts: [
|
|
||||||
{ text: 'Hello ' },
|
|
||||||
{ text: 'there!' },
|
|
||||||
{ functionCall: { name: 'test' } } // Non-text part should be filtered
|
|
||||||
]
|
|
||||||
},
|
|
||||||
finishReason: 'STOP'
|
|
||||||
}],
|
|
||||||
usageMetadata: {
|
|
||||||
promptTokenCount: 5,
|
|
||||||
candidatesTokenCount: 10,
|
|
||||||
totalTokenCount: 15
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = (provider as any).formatCompletionResponse(mockResponse, 'gemini-1.5-flash');
|
|
||||||
|
|
||||||
expect(result.content).toBe('Hello there!');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw error for empty response', () => {
|
it('should throw error for empty response', () => {
|
||||||
const mockResponse = {
|
const mockResponse = {
|
||||||
|
text: '',
|
||||||
candidates: [],
|
candidates: [],
|
||||||
usageMetadata: {
|
usageMetadata: {
|
||||||
promptTokenCount: 5,
|
promptTokenCount: 5,
|
||||||
@@ -349,8 +337,8 @@ describe('GeminiProvider', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
(provider as any).formatCompletionResponse(mockResponse, 'gemini-1.5-flash');
|
(provider as any).formatCompletionResponse(mockResponse, 'gemini-2.5-flash');
|
||||||
}).toThrow('No candidates found in Gemini response');
|
}).toThrow('No text content found in Gemini response');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ describe('OpenAIProvider', () => {
|
|||||||
messages: [{ role: 'user', content: 'test' }],
|
messages: [{ role: 'user', content: 'test' }],
|
||||||
temperature: 1.5
|
temperature: 1.5
|
||||||
})
|
})
|
||||||
).rejects.toThrow('Temperature must be a number between 0.0 and 1.0');
|
).rejects.toThrow(/temperature must be at most 1/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should validate top_p range', async () => {
|
it('should validate top_p range', async () => {
|
||||||
@@ -85,7 +85,7 @@ describe('OpenAIProvider', () => {
|
|||||||
messages: [{ role: 'user', content: 'test' }],
|
messages: [{ role: 'user', content: 'test' }],
|
||||||
topP: 1.5
|
topP: 1.5
|
||||||
})
|
})
|
||||||
).rejects.toThrow('Top-p must be a number between 0.0 (exclusive) and 1.0 (inclusive)');
|
).rejects.toThrow(/topP must be at most 1/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should validate message format', async () => {
|
it('should validate message format', async () => {
|
||||||
@@ -120,22 +120,24 @@ describe('OpenAIProvider', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('error handling', () => {
|
describe('error handling', () => {
|
||||||
|
const mapError = (err: any) => (provider as any).normalizeError(err);
|
||||||
|
|
||||||
it('should handle authentication errors', () => {
|
it('should handle authentication errors', () => {
|
||||||
const error = new Error('Unauthorized');
|
const error = new Error('Unauthorized');
|
||||||
(error as any).status = 401;
|
(error as any).status = 401;
|
||||||
|
|
||||||
const providerError = (provider as any).handleOpenAIError(error);
|
const providerError = mapError(error);
|
||||||
|
|
||||||
expect(providerError).toBeInstanceOf(AIProviderError);
|
expect(providerError).toBeInstanceOf(AIProviderError);
|
||||||
expect(providerError.type).toBe(AIErrorType.AUTHENTICATION);
|
expect(providerError.type).toBe(AIErrorType.AUTHENTICATION);
|
||||||
expect(providerError.message).toContain('Authentication failed');
|
expect(providerError.message).toContain('OpenAI API key');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle rate limit errors', () => {
|
it('should handle rate limit errors', () => {
|
||||||
const error = new Error('Rate limited');
|
const error = new Error('Rate limited');
|
||||||
(error as any).status = 429;
|
(error as any).status = 429;
|
||||||
|
|
||||||
const providerError = (provider as any).handleOpenAIError(error);
|
const providerError = mapError(error);
|
||||||
|
|
||||||
expect(providerError).toBeInstanceOf(AIProviderError);
|
expect(providerError).toBeInstanceOf(AIProviderError);
|
||||||
expect(providerError.type).toBe(AIErrorType.RATE_LIMIT);
|
expect(providerError.type).toBe(AIErrorType.RATE_LIMIT);
|
||||||
@@ -146,7 +148,7 @@ describe('OpenAIProvider', () => {
|
|||||||
const error = new Error('Model not found');
|
const error = new Error('Model not found');
|
||||||
(error as any).status = 404;
|
(error as any).status = 404;
|
||||||
|
|
||||||
const providerError = (provider as any).handleOpenAIError(error);
|
const providerError = mapError(error);
|
||||||
|
|
||||||
expect(providerError).toBeInstanceOf(AIProviderError);
|
expect(providerError).toBeInstanceOf(AIProviderError);
|
||||||
expect(providerError.type).toBe(AIErrorType.MODEL_NOT_FOUND);
|
expect(providerError.type).toBe(AIErrorType.MODEL_NOT_FOUND);
|
||||||
@@ -157,7 +159,7 @@ describe('OpenAIProvider', () => {
|
|||||||
const error = new Error('Bad request');
|
const error = new Error('Bad request');
|
||||||
(error as any).status = 400;
|
(error as any).status = 400;
|
||||||
|
|
||||||
const providerError = (provider as any).handleOpenAIError(error);
|
const providerError = mapError(error);
|
||||||
|
|
||||||
expect(providerError).toBeInstanceOf(AIProviderError);
|
expect(providerError).toBeInstanceOf(AIProviderError);
|
||||||
expect(providerError.type).toBe(AIErrorType.INVALID_REQUEST);
|
expect(providerError.type).toBe(AIErrorType.INVALID_REQUEST);
|
||||||
@@ -167,7 +169,7 @@ describe('OpenAIProvider', () => {
|
|||||||
const error = new Error('Internal server error');
|
const error = new Error('Internal server error');
|
||||||
(error as any).status = 500;
|
(error as any).status = 500;
|
||||||
|
|
||||||
const providerError = (provider as any).handleOpenAIError(error);
|
const providerError = mapError(error);
|
||||||
|
|
||||||
expect(providerError).toBeInstanceOf(AIProviderError);
|
expect(providerError).toBeInstanceOf(AIProviderError);
|
||||||
expect(providerError.type).toBe(AIErrorType.NETWORK);
|
expect(providerError.type).toBe(AIErrorType.NETWORK);
|
||||||
@@ -176,11 +178,21 @@ describe('OpenAIProvider', () => {
|
|||||||
it('should handle unknown errors', () => {
|
it('should handle unknown errors', () => {
|
||||||
const error = new Error('Unknown error');
|
const error = new Error('Unknown error');
|
||||||
|
|
||||||
const providerError = (provider as any).handleOpenAIError(error);
|
const providerError = mapError(error);
|
||||||
|
|
||||||
expect(providerError).toBeInstanceOf(AIProviderError);
|
expect(providerError).toBeInstanceOf(AIProviderError);
|
||||||
expect(providerError.type).toBe(AIErrorType.UNKNOWN);
|
expect(providerError.type).toBe(AIErrorType.UNKNOWN);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should route 429 with quota to quota-specific message', () => {
|
||||||
|
const error = new Error('quota exceeded');
|
||||||
|
(error as any).status = 429;
|
||||||
|
|
||||||
|
const providerError = mapError(error);
|
||||||
|
|
||||||
|
expect(providerError.type).toBe(AIErrorType.RATE_LIMIT);
|
||||||
|
expect(providerError.message).toContain('Usage quota exceeded');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('message conversion', () => {
|
describe('message conversion', () => {
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* Tests for OpenWebUI provider implementation
|
* Tests for OpenWebUI provider implementation
|
||||||
|
*
|
||||||
|
* The R3 refactor split message conversion, response formatting, and the
|
||||||
|
* HTTP client into openwebui-strategies.ts and openwebui-http.ts. Tests for
|
||||||
|
* those helpers should live alongside those files. This file covers the
|
||||||
|
* provider's public surface and the error-mapping hooks it adds on top of
|
||||||
|
* BaseAIProvider.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, beforeEach, jest } from 'bun:test';
|
import { describe, it, expect, beforeEach, jest } from 'bun:test';
|
||||||
@@ -14,7 +20,7 @@ describe('OpenWebUIProvider', () => {
|
|||||||
mockConfig = {
|
mockConfig = {
|
||||||
apiKey: 'test-bearer-token',
|
apiKey: 'test-bearer-token',
|
||||||
baseUrl: 'http://localhost:3000',
|
baseUrl: 'http://localhost:3000',
|
||||||
defaultModel: 'llama3.1'
|
defaultModel: 'llama3.1:latest'
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -26,31 +32,32 @@ describe('OpenWebUIProvider', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should use default values when not specified', () => {
|
it('should use default values when not specified', () => {
|
||||||
const minimalConfig: OpenWebUIConfig = { apiKey: 'test' };
|
provider = new OpenWebUIProvider({ apiKey: 'test' });
|
||||||
provider = new OpenWebUIProvider(minimalConfig);
|
|
||||||
expect(provider).toBeInstanceOf(OpenWebUIProvider);
|
expect(provider).toBeInstanceOf(OpenWebUIProvider);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle custom configuration', () => {
|
it('should accept Ollama proxy mode', () => {
|
||||||
const config: OpenWebUIConfig = {
|
provider = new OpenWebUIProvider({
|
||||||
apiKey: 'test',
|
apiKey: 'test',
|
||||||
baseUrl: 'http://localhost:8080',
|
baseUrl: 'http://localhost:8080',
|
||||||
defaultModel: 'mistral:7b',
|
defaultModel: 'mistral:7b',
|
||||||
useOllamaProxy: true
|
useOllamaProxy: true
|
||||||
};
|
});
|
||||||
provider = new OpenWebUIProvider(config);
|
|
||||||
expect(provider).toBeInstanceOf(OpenWebUIProvider);
|
expect(provider).toBeInstanceOf(OpenWebUIProvider);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle trailing slash in baseUrl', () => {
|
it('should normalize trailing slash in baseUrl', () => {
|
||||||
const config: OpenWebUIConfig = {
|
provider = new OpenWebUIProvider({
|
||||||
apiKey: 'test',
|
apiKey: 'test',
|
||||||
baseUrl: 'http://localhost:3000/',
|
baseUrl: 'http://localhost:3000/'
|
||||||
defaultModel: 'llama3.1'
|
});
|
||||||
};
|
|
||||||
provider = new OpenWebUIProvider(config);
|
|
||||||
expect(provider).toBeInstanceOf(OpenWebUIProvider);
|
expect(provider).toBeInstanceOf(OpenWebUIProvider);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should reject malformed baseUrl', () => {
|
||||||
|
expect(() => new OpenWebUIProvider({ apiKey: 'test', baseUrl: 'not a url' }))
|
||||||
|
.toThrow(AIProviderError);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getInfo', () => {
|
describe('getInfo', () => {
|
||||||
@@ -62,7 +69,7 @@ describe('OpenWebUIProvider', () => {
|
|||||||
const info = provider.getInfo();
|
const info = provider.getInfo();
|
||||||
|
|
||||||
expect(info.name).toBe('OpenWebUI');
|
expect(info.name).toBe('OpenWebUI');
|
||||||
expect(info.version).toBe('1.0.0');
|
expect(info.version).toBe('2.0.0');
|
||||||
expect(info.supportsStreaming).toBe(true);
|
expect(info.supportsStreaming).toBe(true);
|
||||||
expect(info.maxContextLength).toBe(32768);
|
expect(info.maxContextLength).toBe(32768);
|
||||||
expect(Array.isArray(info.models)).toBe(true);
|
expect(Array.isArray(info.models)).toBe(true);
|
||||||
@@ -77,338 +84,113 @@ describe('OpenWebUIProvider', () => {
|
|||||||
expect(info.models).toContain('gemma:latest');
|
expect(info.models).toContain('gemma:latest');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have correct capabilities', () => {
|
it('should expose capabilities', () => {
|
||||||
const info = provider.getInfo();
|
const info = provider.getInfo();
|
||||||
|
|
||||||
expect(info.capabilities).toEqual({
|
expect(info.capabilities).toMatchObject({
|
||||||
vision: false,
|
vision: false,
|
||||||
functionCalling: false,
|
functionCalling: false,
|
||||||
jsonMode: false,
|
|
||||||
systemMessages: true,
|
systemMessages: true,
|
||||||
reasoning: true,
|
localDeployment: true
|
||||||
codeGeneration: true,
|
|
||||||
localDeployment: true,
|
|
||||||
multiModel: true
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Error Handling', () => {
|
describe('Error mapping', () => {
|
||||||
|
const mapError = (err: any) => (provider as any).normalizeError(err);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
provider = new OpenWebUIProvider(mockConfig);
|
provider = new OpenWebUIProvider(mockConfig);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle connection refused errors', () => {
|
it('should map ECONNREFUSED to NETWORK', () => {
|
||||||
const mockError = new Error('connect ECONNREFUSED') as any;
|
const error = new Error('connect ECONNREFUSED') as any;
|
||||||
mockError.code = 'ECONNREFUSED';
|
|
||||||
|
|
||||||
const aiError = (provider as any).handleOpenWebUIError(mockError);
|
const aiError = mapError(error);
|
||||||
|
|
||||||
expect(aiError).toBeInstanceOf(AIProviderError);
|
expect(aiError).toBeInstanceOf(AIProviderError);
|
||||||
expect(aiError.type).toBe(AIErrorType.NETWORK);
|
expect(aiError.type).toBe(AIErrorType.NETWORK);
|
||||||
expect(aiError.message).toContain('Network error connecting to OpenWebUI');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle hostname resolution errors', () => {
|
it('should map ENOTFOUND to NETWORK', () => {
|
||||||
const mockError = new Error('getaddrinfo ENOTFOUND') as any;
|
const error = new Error('getaddrinfo ENOTFOUND') as any;
|
||||||
mockError.code = 'ENOTFOUND';
|
|
||||||
|
|
||||||
const aiError = (provider as any).handleOpenWebUIError(mockError);
|
const aiError = mapError(error);
|
||||||
|
|
||||||
expect(aiError).toBeInstanceOf(AIProviderError);
|
expect(aiError.type).toBe(AIErrorType.NETWORK);
|
||||||
expect(aiError.type).toBe(AIErrorType.UNKNOWN);
|
|
||||||
expect(aiError.message).toContain('OpenWebUI error');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle 404 model not found errors', () => {
|
it('should map 404 with "model" in message to MODEL_NOT_FOUND', () => {
|
||||||
const mockError = new Error('model not available') as any;
|
const error = new Error('model not available') as any;
|
||||||
mockError.status = 404;
|
error.status = 404;
|
||||||
mockError.message = 'model not available';
|
|
||||||
|
|
||||||
const aiError = (provider as any).handleOpenWebUIError(mockError);
|
const aiError = mapError(error);
|
||||||
|
|
||||||
expect(aiError).toBeInstanceOf(AIProviderError);
|
|
||||||
expect(aiError.type).toBe(AIErrorType.MODEL_NOT_FOUND);
|
expect(aiError.type).toBe(AIErrorType.MODEL_NOT_FOUND);
|
||||||
|
expect(aiError.message).toContain('OpenWebUI');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle 404 endpoint not found errors', () => {
|
it('should map 404 without "model" in message to MODEL_NOT_FOUND with endpoint message', () => {
|
||||||
const mockError = new Error('Not found') as any;
|
const error = new Error('Not found') as any;
|
||||||
mockError.status = 404;
|
error.status = 404;
|
||||||
mockError.message = 'endpoint not found';
|
|
||||||
|
|
||||||
const aiError = (provider as any).handleOpenWebUIError(mockError);
|
const aiError = mapError(error);
|
||||||
|
|
||||||
expect(aiError).toBeInstanceOf(AIProviderError);
|
expect(aiError.type).toBe(AIErrorType.MODEL_NOT_FOUND);
|
||||||
expect(aiError.type).toBe(AIErrorType.INVALID_REQUEST);
|
expect(aiError.message).toContain('endpoint');
|
||||||
expect(aiError.message).toContain('OpenWebUI endpoint not found');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle timeout errors', () => {
|
it('should map timeout errors to TIMEOUT', () => {
|
||||||
const mockError = new Error('Request timeout') as any;
|
const error = new Error('Request timeout') as any;
|
||||||
mockError.code = 'ETIMEDOUT';
|
|
||||||
|
|
||||||
const aiError = (provider as any).handleOpenWebUIError(mockError);
|
const aiError = mapError(error);
|
||||||
|
|
||||||
expect(aiError).toBeInstanceOf(AIProviderError);
|
|
||||||
expect(aiError.type).toBe(AIErrorType.TIMEOUT);
|
expect(aiError.type).toBe(AIErrorType.TIMEOUT);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle rate limit errors', () => {
|
it('should map 429 to RATE_LIMIT', () => {
|
||||||
const mockError = new Error('Rate limit exceeded') as any;
|
const error = new Error('Rate limit exceeded') as any;
|
||||||
mockError.status = 429;
|
error.status = 429;
|
||||||
|
|
||||||
const aiError = (provider as any).handleOpenWebUIError(mockError);
|
const aiError = mapError(error);
|
||||||
|
|
||||||
expect(aiError).toBeInstanceOf(AIProviderError);
|
|
||||||
expect(aiError.type).toBe(AIErrorType.RATE_LIMIT);
|
expect(aiError.type).toBe(AIErrorType.RATE_LIMIT);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle server errors', () => {
|
it('should map 500 to NETWORK', () => {
|
||||||
const mockError = new Error('Internal server error') as any;
|
const error = new Error('Internal server error') as any;
|
||||||
mockError.status = 500;
|
error.status = 500;
|
||||||
|
|
||||||
const aiError = (provider as any).handleOpenWebUIError(mockError);
|
const aiError = mapError(error);
|
||||||
|
|
||||||
expect(aiError).toBeInstanceOf(AIProviderError);
|
|
||||||
expect(aiError.type).toBe(AIErrorType.NETWORK);
|
expect(aiError.type).toBe(AIErrorType.NETWORK);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle authentication errors', () => {
|
it('should map 401 to AUTHENTICATION with OpenWebUI hint', () => {
|
||||||
const mockError = new Error('Unauthorized') as any;
|
const error = new Error('Unauthorized') as any;
|
||||||
mockError.status = 401;
|
error.status = 401;
|
||||||
|
|
||||||
const aiError = (provider as any).handleOpenWebUIError(mockError);
|
const aiError = mapError(error);
|
||||||
|
|
||||||
expect(aiError).toBeInstanceOf(AIProviderError);
|
|
||||||
expect(aiError.type).toBe(AIErrorType.AUTHENTICATION);
|
expect(aiError.type).toBe(AIErrorType.AUTHENTICATION);
|
||||||
expect(aiError.message).toContain('Authentication failed with OpenWebUI');
|
expect(aiError.message).toContain('OpenWebUI');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle unknown errors', () => {
|
it('should map unrecognized errors to UNKNOWN', () => {
|
||||||
const mockError = new Error('Unknown error');
|
const error = new Error('Unknown error');
|
||||||
|
|
||||||
const aiError = (provider as any).handleOpenWebUIError(mockError);
|
const aiError = mapError(error);
|
||||||
|
|
||||||
expect(aiError).toBeInstanceOf(AIProviderError);
|
|
||||||
expect(aiError.type).toBe(AIErrorType.UNKNOWN);
|
expect(aiError.type).toBe(AIErrorType.UNKNOWN);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Message Conversion', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
provider = new OpenWebUIProvider(mockConfig);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should convert messages correctly for chat API', () => {
|
|
||||||
const messages = [
|
|
||||||
{ role: 'system' as const, content: 'You are helpful' },
|
|
||||||
{ role: 'user' as const, content: 'Hello' },
|
|
||||||
{ role: 'assistant' as const, content: 'Hi there!' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const converted = (provider as any).convertMessages(messages);
|
|
||||||
|
|
||||||
expect(converted).toHaveLength(3);
|
|
||||||
expect(converted[0]).toEqual({ role: 'system', content: 'You are helpful' });
|
|
||||||
expect(converted[1]).toEqual({ role: 'user', content: 'Hello' });
|
|
||||||
expect(converted[2]).toEqual({ role: 'assistant', content: 'Hi there!' });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should convert messages to prompt for Ollama API', () => {
|
|
||||||
const messages = [
|
|
||||||
{ role: 'system' as const, content: 'You are helpful' },
|
|
||||||
{ role: 'user' as const, content: 'Hello' },
|
|
||||||
{ role: 'assistant' as const, content: 'Hi there!' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const prompt = (provider as any).convertMessagesToPrompt(messages);
|
|
||||||
|
|
||||||
expect(prompt).toBe('System: You are helpful\n\nHuman: Hello\n\nAssistant: Hi there!\n\nAssistant: ');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Response Formatting', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
provider = new OpenWebUIProvider(mockConfig);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should format chat completion response correctly', () => {
|
|
||||||
const mockResponse = {
|
|
||||||
id: 'test-id',
|
|
||||||
object: 'chat.completion',
|
|
||||||
created: 1234567890,
|
|
||||||
model: 'llama3.1',
|
|
||||||
choices: [{
|
|
||||||
index: 0,
|
|
||||||
message: { role: 'assistant', content: 'Hello there!' },
|
|
||||||
finish_reason: 'stop'
|
|
||||||
}],
|
|
||||||
usage: {
|
|
||||||
prompt_tokens: 10,
|
|
||||||
completion_tokens: 5,
|
|
||||||
total_tokens: 15
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = (provider as any).formatChatResponse(mockResponse);
|
|
||||||
|
|
||||||
expect(response.content).toBe('Hello there!');
|
|
||||||
expect(response.model).toBe('llama3.1');
|
|
||||||
expect(response.id).toBe('test-id');
|
|
||||||
expect(response.usage.promptTokens).toBe(10);
|
|
||||||
expect(response.usage.completionTokens).toBe(5);
|
|
||||||
expect(response.usage.totalTokens).toBe(15);
|
|
||||||
expect(response.metadata?.finishReason).toBe('stop');
|
|
||||||
expect(response.metadata?.created).toBe(1234567890);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should format Ollama response correctly', () => {
|
|
||||||
const mockResponse = {
|
|
||||||
model: 'llama3.1',
|
|
||||||
created_at: '2024-01-01T00:00:00Z',
|
|
||||||
response: 'Hello there!',
|
|
||||||
done: true,
|
|
||||||
prompt_eval_count: 10,
|
|
||||||
eval_count: 5,
|
|
||||||
total_duration: 1000000,
|
|
||||||
eval_duration: 500000
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = (provider as any).formatOllamaResponse(mockResponse);
|
|
||||||
|
|
||||||
expect(response.content).toBe('Hello there!');
|
|
||||||
expect(response.model).toBe('llama3.1');
|
|
||||||
expect(response.usage.promptTokens).toBe(10);
|
|
||||||
expect(response.usage.completionTokens).toBe(5);
|
|
||||||
expect(response.usage.totalTokens).toBe(15);
|
|
||||||
expect(response.metadata?.created).toBe(1704067200000);
|
|
||||||
expect(response.metadata?.totalDuration).toBe(1000000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle response without content', () => {
|
|
||||||
const mockResponse = {
|
|
||||||
id: 'test-id',
|
|
||||||
object: 'chat.completion',
|
|
||||||
created: 1234567890,
|
|
||||||
model: 'llama3.1',
|
|
||||||
choices: [{
|
|
||||||
index: 0,
|
|
||||||
message: { role: 'assistant' }, // Message without content
|
|
||||||
finish_reason: 'stop'
|
|
||||||
}],
|
|
||||||
usage: { prompt_tokens: 5, completion_tokens: 0, total_tokens: 5 }
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
(provider as any).formatChatResponse(mockResponse);
|
|
||||||
}).toThrow(AIProviderError);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle missing usage information', () => {
|
|
||||||
const mockResponse = {
|
|
||||||
id: 'test-id',
|
|
||||||
object: 'chat.completion',
|
|
||||||
created: 1234567890,
|
|
||||||
model: 'llama3.1',
|
|
||||||
choices: [{
|
|
||||||
index: 0,
|
|
||||||
message: { role: 'assistant', content: 'Hello there!' },
|
|
||||||
finish_reason: 'stop'
|
|
||||||
}]
|
|
||||||
// No usage information
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = (provider as any).formatChatResponse(mockResponse);
|
|
||||||
|
|
||||||
expect(response.content).toBe('Hello there!');
|
|
||||||
expect(response.usage.promptTokens).toBe(0);
|
|
||||||
expect(response.usage.completionTokens).toBe(0);
|
|
||||||
expect(response.usage.totalTokens).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Configuration Options', () => {
|
|
||||||
it('should handle custom baseUrl', () => {
|
|
||||||
const config: OpenWebUIConfig = {
|
|
||||||
apiKey: 'test',
|
|
||||||
baseUrl: 'https://my-openwebui.com'
|
|
||||||
};
|
|
||||||
|
|
||||||
provider = new OpenWebUIProvider(config);
|
|
||||||
expect(provider).toBeInstanceOf(OpenWebUIProvider);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle custom default model', () => {
|
|
||||||
const config: OpenWebUIConfig = {
|
|
||||||
apiKey: 'test',
|
|
||||||
defaultModel: 'mistral:7b'
|
|
||||||
};
|
|
||||||
|
|
||||||
provider = new OpenWebUIProvider(config);
|
|
||||||
expect(provider).toBeInstanceOf(OpenWebUIProvider);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle Ollama proxy mode', () => {
|
|
||||||
const config: OpenWebUIConfig = {
|
|
||||||
apiKey: 'test',
|
|
||||||
useOllamaProxy: true
|
|
||||||
};
|
|
||||||
|
|
||||||
provider = new OpenWebUIProvider(config);
|
|
||||||
expect(provider).toBeInstanceOf(OpenWebUIProvider);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle SSL verification settings', () => {
|
|
||||||
const config: OpenWebUIConfig = {
|
|
||||||
apiKey: 'test',
|
|
||||||
dangerouslyAllowInsecureConnections: false
|
|
||||||
};
|
|
||||||
|
|
||||||
provider = new OpenWebUIProvider(config);
|
|
||||||
expect(provider).toBeInstanceOf(OpenWebUIProvider);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Request Generation', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
provider = new OpenWebUIProvider(mockConfig);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should generate correct headers', () => {
|
|
||||||
const headers = (provider as any).makeRequest('http://test.com', 'GET');
|
|
||||||
// Note: This would need to be mocked in a real test environment
|
|
||||||
expect(typeof headers).toBe('object');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('API Mode Selection', () => {
|
|
||||||
it('should use chat API by default', () => {
|
|
||||||
const config: OpenWebUIConfig = {
|
|
||||||
apiKey: 'test',
|
|
||||||
useOllamaProxy: false
|
|
||||||
};
|
|
||||||
|
|
||||||
provider = new OpenWebUIProvider(config);
|
|
||||||
expect(provider).toBeInstanceOf(OpenWebUIProvider);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should use Ollama proxy when configured', () => {
|
|
||||||
const config: OpenWebUIConfig = {
|
|
||||||
apiKey: 'test',
|
|
||||||
useOllamaProxy: true
|
|
||||||
};
|
|
||||||
|
|
||||||
provider = new OpenWebUIProvider(config);
|
|
||||||
expect(provider).toBeInstanceOf(OpenWebUIProvider);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Validation', () => {
|
describe('Validation', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
provider = new OpenWebUIProvider(mockConfig);
|
provider = new OpenWebUIProvider(mockConfig);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should validate provider is initialized before use', async () => {
|
it('should require initialization before use', async () => {
|
||||||
const params: CompletionParams = {
|
const params: CompletionParams = {
|
||||||
messages: [{ role: 'user', content: 'Hello' }]
|
messages: [{ role: 'user', content: 'Hello' }]
|
||||||
};
|
};
|
||||||
@@ -417,31 +199,31 @@ describe('OpenWebUIProvider', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('structured responses', () => {
|
describe('Structured responses', () => {
|
||||||
it('should handle structured responses correctly', async () => {
|
it('should parse JSON content into typed responses', async () => {
|
||||||
const provider = new OpenWebUIProvider({ apiKey: 'test-key' });
|
const p = new OpenWebUIProvider({ apiKey: 'test-key' });
|
||||||
const mockResponse = {
|
const mockResponse = {
|
||||||
content: JSON.stringify({ name: 'John Doe', age: 30 }),
|
content: JSON.stringify({ name: 'John Doe', age: 30 }),
|
||||||
model: 'llama3.1',
|
model: 'llama3.1:latest',
|
||||||
usage: { promptTokens: 10, completionTokens: 20, totalTokens: 30 },
|
usage: { promptTokens: 10, completionTokens: 20, totalTokens: 30 },
|
||||||
id: 'test-id'
|
id: 'test-id'
|
||||||
};
|
};
|
||||||
(provider as any).doComplete = jest.fn().mockResolvedValue(mockResponse);
|
(p as any).doComplete = jest.fn().mockResolvedValue(mockResponse);
|
||||||
(provider as any).initialized = true;
|
(p as any).initialized = true;
|
||||||
|
|
||||||
const responseType = createResponseType<{ name: string; age: number }>
|
const responseType = createResponseType<{ name: string; age: number }>(
|
||||||
(`{ name: string; age: number }`,
|
`{ name: string; age: number }`,
|
||||||
'A user profile'
|
'A user profile'
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await provider.complete<{ name: string; age: number }>({
|
const response = await p.complete<{ name: string; age: number }>({
|
||||||
messages: [{ role: 'user', content: 'test' }],
|
messages: [{ role: 'user', content: 'test' }],
|
||||||
responseType
|
responseType
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(response.content).toEqual({ name: 'John Doe', age: 30 });
|
expect(response.content).toEqual({ name: 'John Doe', age: 30 });
|
||||||
expect(response.rawContent).toBe(JSON.stringify({ name: 'John Doe', age: 30 }));
|
expect(response.rawContent).toBe(JSON.stringify({ name: 'John Doe', age: 30 }));
|
||||||
expect((provider as any).doComplete).toHaveBeenCalled();
|
expect((p as any).doComplete).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user