mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-09 17:31:52 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf3fdd10f1 | ||
|
|
4fdc51c6ed |
No files matched your search
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=2.7.8" \
|
||||
"praisonai>=2.7.9" \
|
||||
"praisonai[chat]" \
|
||||
"embedchain[github,youtube]"
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=2.7.8" \
|
||||
"praisonai>=2.7.9" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[chat]" \
|
||||
"praisonai[realtime]" \
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=2.7.8" \
|
||||
"praisonai>=2.7.9" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
"""
|
||||
Custom Provider Registry Example (Python)
|
||||
|
||||
This example demonstrates how to register and use custom LLM providers
|
||||
with the PraisonAI Python wrapper.
|
||||
|
||||
Note: The Python provider registry is for custom provider extensions.
|
||||
Built-in providers (OpenAI, Anthropic, Google) are handled by LiteLLM
|
||||
in praisonaiagents automatically.
|
||||
"""
|
||||
|
||||
import sys
|
||||
sys.path.insert(0, '../../src/praisonai')
|
||||
|
||||
from praisonai.llm import (
|
||||
LLMProviderRegistry,
|
||||
register_llm_provider,
|
||||
unregister_llm_provider,
|
||||
has_llm_provider,
|
||||
list_llm_providers,
|
||||
create_llm_provider,
|
||||
get_default_llm_registry,
|
||||
parse_model_string
|
||||
)
|
||||
|
||||
|
||||
# Example 1: Simple Custom Provider
|
||||
# ---------------------------------
|
||||
|
||||
class SimpleCustomProvider:
|
||||
"""A minimal custom provider example."""
|
||||
|
||||
provider_id = "simple-custom"
|
||||
|
||||
def __init__(self, model_id: str, config: dict = None):
|
||||
self.model_id = model_id
|
||||
self.config = config or {}
|
||||
self.api_endpoint = self.config.get('api_endpoint', 'https://api.example.com')
|
||||
|
||||
def generate(self, prompt: str) -> str:
|
||||
"""Generate a response (simulated)."""
|
||||
print(f"[SimpleCustomProvider] Generating with model: {self.model_id}")
|
||||
print(f"[SimpleCustomProvider] Prompt: {prompt[:50]}...")
|
||||
return f"Response from {self.provider_id}/{self.model_id}: Hello! This is a simulated response."
|
||||
|
||||
|
||||
# Example 2: Ollama Provider
|
||||
# --------------------------
|
||||
|
||||
class OllamaProvider:
|
||||
"""Custom provider for local Ollama integration."""
|
||||
|
||||
provider_id = "ollama"
|
||||
|
||||
def __init__(self, model_id: str, config: dict = None):
|
||||
self.model_id = model_id
|
||||
self.config = config or {}
|
||||
self.base_url = self.config.get('base_url', 'http://localhost:11434')
|
||||
|
||||
def generate(self, prompt: str) -> str:
|
||||
"""Generate a response using Ollama API."""
|
||||
import requests
|
||||
|
||||
response = requests.post(
|
||||
f"{self.base_url}/api/generate",
|
||||
json={
|
||||
"model": self.model_id,
|
||||
"prompt": prompt,
|
||||
"stream": False
|
||||
}
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
raise Exception(f"Ollama API error: {response.text}")
|
||||
|
||||
return response.json().get("response", "")
|
||||
|
||||
def generate_stream(self, prompt: str):
|
||||
"""Generate a streaming response using Ollama API."""
|
||||
import requests
|
||||
|
||||
response = requests.post(
|
||||
f"{self.base_url}/api/generate",
|
||||
json={
|
||||
"model": self.model_id,
|
||||
"prompt": prompt,
|
||||
"stream": True
|
||||
},
|
||||
stream=True
|
||||
)
|
||||
|
||||
for line in response.iter_lines():
|
||||
if line:
|
||||
import json
|
||||
data = json.loads(line)
|
||||
yield data.get("response", "")
|
||||
if data.get("done"):
|
||||
break
|
||||
|
||||
|
||||
# Example 3: Cloudflare Workers AI Provider
|
||||
# -----------------------------------------
|
||||
|
||||
class CloudflareProvider:
|
||||
"""Custom provider for Cloudflare Workers AI."""
|
||||
|
||||
provider_id = "cloudflare"
|
||||
|
||||
def __init__(self, model_id: str, config: dict = None):
|
||||
self.model_id = model_id
|
||||
self.config = config or {}
|
||||
self.account_id = self.config.get('account_id')
|
||||
self.api_token = self.config.get('api_token')
|
||||
|
||||
def generate(self, prompt: str) -> str:
|
||||
"""Generate a response using Cloudflare Workers AI."""
|
||||
import requests
|
||||
|
||||
if not self.account_id or not self.api_token:
|
||||
raise ValueError("Cloudflare account_id and api_token are required")
|
||||
|
||||
response = requests.post(
|
||||
f"https://api.cloudflare.com/client/v4/accounts/{self.account_id}/ai/run/{self.model_id}",
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.api_token}",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
json={"prompt": prompt}
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
raise Exception(f"Cloudflare API error: {response.text}")
|
||||
|
||||
return response.json().get("result", {}).get("response", "")
|
||||
|
||||
|
||||
def main():
|
||||
print("=== Provider Registry Example (Python) ===\n")
|
||||
|
||||
# Check initial state
|
||||
print("Initial providers:", list_llm_providers())
|
||||
print()
|
||||
|
||||
# Register custom providers
|
||||
print("Registering custom providers...")
|
||||
register_llm_provider("simple-custom", SimpleCustomProvider)
|
||||
register_llm_provider("ollama", OllamaProvider, aliases=["local"])
|
||||
register_llm_provider("cloudflare", CloudflareProvider, aliases=["cf", "workers-ai"])
|
||||
print()
|
||||
|
||||
# Check providers after registration
|
||||
print("Providers after registration:", list_llm_providers())
|
||||
print("Has ollama:", has_llm_provider("ollama"))
|
||||
print("Has local (alias):", has_llm_provider("local"))
|
||||
print("Has cloudflare:", has_llm_provider("cloudflare"))
|
||||
print("Has cf (alias):", has_llm_provider("cf"))
|
||||
print()
|
||||
|
||||
# Parse model strings
|
||||
print("=== Model String Parsing ===\n")
|
||||
|
||||
test_strings = [
|
||||
"openai/gpt-4o-mini",
|
||||
"gpt-4o-mini",
|
||||
"claude-3-5-sonnet-latest",
|
||||
"gemini-2.0-flash",
|
||||
"ollama/llama2",
|
||||
"cloudflare/workers-ai-model"
|
||||
]
|
||||
|
||||
for model_str in test_strings:
|
||||
parsed = parse_model_string(model_str)
|
||||
print(f" '{model_str}' -> provider={parsed['provider_id']}, model={parsed['model_id']}")
|
||||
print()
|
||||
|
||||
# Create and use providers
|
||||
print("=== Using Custom Providers ===\n")
|
||||
|
||||
# Use simple custom provider
|
||||
provider = create_llm_provider("simple-custom/test-model")
|
||||
print(f"Created provider: {provider.provider_id}/{provider.model_id}")
|
||||
response = provider.generate("Hello, world!")
|
||||
print(f"Response: {response}")
|
||||
print()
|
||||
|
||||
# Use ollama provider via alias
|
||||
provider = create_llm_provider("local/llama2", config={"base_url": "http://localhost:11434"})
|
||||
print(f"Created provider: {provider.provider_id}/{provider.model_id}")
|
||||
print()
|
||||
|
||||
# Demonstrate error handling
|
||||
print("=== Error Handling ===\n")
|
||||
try:
|
||||
create_llm_provider("unknown-provider/model")
|
||||
except ValueError as e:
|
||||
print(f"Expected error: {e}")
|
||||
print()
|
||||
|
||||
# Demonstrate isolated registries
|
||||
print("=== Isolated Registries ===\n")
|
||||
|
||||
# Create isolated registry
|
||||
isolated_registry = LLMProviderRegistry()
|
||||
isolated_registry.register("isolated-provider", SimpleCustomProvider)
|
||||
|
||||
print(f"Default registry providers: {list_llm_providers()}")
|
||||
print(f"Isolated registry providers: {isolated_registry.list()}")
|
||||
|
||||
# Use isolated registry
|
||||
provider = create_llm_provider("isolated-provider/model", registry=isolated_registry)
|
||||
print(f"Created from isolated registry: {provider.provider_id}/{provider.model_id}")
|
||||
print()
|
||||
|
||||
# Cleanup
|
||||
print("=== Cleanup ===\n")
|
||||
unregister_llm_provider("simple-custom")
|
||||
unregister_llm_provider("ollama")
|
||||
unregister_llm_provider("cloudflare")
|
||||
print(f"Providers after cleanup: {list_llm_providers()}")
|
||||
|
||||
print("\n=== Example Complete ===")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* Custom Provider Registry Example
|
||||
*
|
||||
* This example demonstrates how to register and use custom LLM providers
|
||||
* with the PraisonAI TypeScript SDK.
|
||||
*/
|
||||
|
||||
import {
|
||||
registerProvider,
|
||||
createProvider,
|
||||
BaseProvider,
|
||||
listProviders,
|
||||
hasProvider,
|
||||
type ProviderConfig,
|
||||
type GenerateTextOptions,
|
||||
type GenerateTextResult,
|
||||
type StreamTextOptions,
|
||||
type StreamChunk,
|
||||
type GenerateObjectOptions,
|
||||
type GenerateObjectResult,
|
||||
type Message,
|
||||
type ToolDefinition
|
||||
} from 'praisonai';
|
||||
|
||||
// Example 1: Simple Custom Provider
|
||||
// ---------------------------------
|
||||
// A minimal custom provider that wraps a hypothetical API
|
||||
|
||||
class SimpleCustomProvider extends BaseProvider {
|
||||
readonly providerId = 'simple-custom';
|
||||
private apiEndpoint: string;
|
||||
|
||||
constructor(modelId: string, config?: ProviderConfig) {
|
||||
super(modelId, config);
|
||||
this.apiEndpoint = (config as any)?.apiEndpoint || 'https://api.example.com';
|
||||
}
|
||||
|
||||
async generateText(options: GenerateTextOptions): Promise<GenerateTextResult> {
|
||||
// In a real implementation, you would call your API here
|
||||
console.log(`[SimpleCustomProvider] Generating text with model: ${this.modelId}`);
|
||||
console.log(`[SimpleCustomProvider] Messages:`, options.messages);
|
||||
|
||||
// Simulated response
|
||||
return {
|
||||
text: `Response from ${this.providerId}/${this.modelId}: Hello! This is a simulated response.`,
|
||||
usage: {
|
||||
promptTokens: 10,
|
||||
completionTokens: 20,
|
||||
totalTokens: 30
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async *streamText(options: StreamTextOptions): AsyncGenerator<StreamChunk> {
|
||||
const words = ['Hello', 'from', 'streaming', 'response!'];
|
||||
for (const word of words) {
|
||||
yield { text: word + ' ', done: false };
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
yield { text: '', done: true };
|
||||
}
|
||||
|
||||
async generateObject<T>(options: GenerateObjectOptions<T>): Promise<GenerateObjectResult<T>> {
|
||||
// Simulated structured output
|
||||
return {
|
||||
object: { message: 'Structured response' } as T,
|
||||
usage: {
|
||||
promptTokens: 10,
|
||||
completionTokens: 20,
|
||||
totalTokens: 30
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
formatTools(tools: ToolDefinition[]): any[] {
|
||||
return tools;
|
||||
}
|
||||
|
||||
formatMessages(messages: Message[]): any[] {
|
||||
return messages;
|
||||
}
|
||||
}
|
||||
|
||||
// Example 2: Ollama Provider
|
||||
// --------------------------
|
||||
// A more realistic example for local Ollama integration
|
||||
|
||||
class OllamaProvider extends BaseProvider {
|
||||
readonly providerId = 'ollama';
|
||||
private baseUrl: string;
|
||||
|
||||
constructor(modelId: string, config?: ProviderConfig) {
|
||||
super(modelId, config);
|
||||
this.baseUrl = (config as any)?.baseUrl || 'http://localhost:11434';
|
||||
}
|
||||
|
||||
async generateText(options: GenerateTextOptions): Promise<GenerateTextResult> {
|
||||
const response = await fetch(`${this.baseUrl}/api/generate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: this.modelId,
|
||||
prompt: options.messages.map(m => `${m.role}: ${m.content}`).join('\n'),
|
||||
stream: false
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Ollama API error: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
text: data.response,
|
||||
usage: {
|
||||
promptTokens: data.prompt_eval_count || 0,
|
||||
completionTokens: data.eval_count || 0,
|
||||
totalTokens: (data.prompt_eval_count || 0) + (data.eval_count || 0)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async *streamText(options: StreamTextOptions): AsyncGenerator<StreamChunk> {
|
||||
const response = await fetch(`${this.baseUrl}/api/generate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: this.modelId,
|
||||
prompt: options.messages.map(m => `${m.role}: ${m.content}`).join('\n'),
|
||||
stream: true
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Ollama API error: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
if (!reader) throw new Error('No response body');
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
const chunk = decoder.decode(value);
|
||||
const lines = chunk.split('\n').filter(line => line.trim());
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const data = JSON.parse(line);
|
||||
yield { text: data.response || '', done: data.done || false };
|
||||
} catch {
|
||||
// Skip invalid JSON
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async generateObject<T>(options: GenerateObjectOptions<T>): Promise<GenerateObjectResult<T>> {
|
||||
const result = await this.generateText({
|
||||
messages: [
|
||||
...options.messages,
|
||||
{ role: 'system', content: `Respond with valid JSON matching this schema: ${JSON.stringify(options.schema)}` }
|
||||
]
|
||||
});
|
||||
|
||||
return {
|
||||
object: JSON.parse(result.text) as T,
|
||||
usage: result.usage
|
||||
};
|
||||
}
|
||||
|
||||
formatTools(tools: ToolDefinition[]): any[] {
|
||||
return tools;
|
||||
}
|
||||
|
||||
formatMessages(messages: Message[]): any[] {
|
||||
return messages;
|
||||
}
|
||||
}
|
||||
|
||||
// Main Example
|
||||
// ------------
|
||||
|
||||
async function main() {
|
||||
console.log('=== Provider Registry Example ===\n');
|
||||
|
||||
// Check initial providers
|
||||
console.log('Initial providers:', listProviders());
|
||||
console.log('Has openai:', hasProvider('openai'));
|
||||
console.log('Has ollama:', hasProvider('ollama'));
|
||||
console.log();
|
||||
|
||||
// Register custom providers
|
||||
console.log('Registering custom providers...');
|
||||
registerProvider('simple-custom', SimpleCustomProvider);
|
||||
registerProvider('ollama', OllamaProvider, { aliases: ['local'] });
|
||||
console.log();
|
||||
|
||||
// Check providers after registration
|
||||
console.log('Providers after registration:', listProviders());
|
||||
console.log('Has ollama:', hasProvider('ollama'));
|
||||
console.log('Has local (alias):', hasProvider('local'));
|
||||
console.log();
|
||||
|
||||
// Create and use providers
|
||||
console.log('=== Using Custom Providers ===\n');
|
||||
|
||||
// Use simple custom provider
|
||||
const simpleProvider = createProvider('simple-custom/test-model');
|
||||
console.log(`Created provider: ${simpleProvider.providerId}/${simpleProvider.modelId}`);
|
||||
|
||||
const simpleResult = await simpleProvider.generateText({
|
||||
messages: [{ role: 'user', content: 'Hello!' }]
|
||||
});
|
||||
console.log('Response:', simpleResult.text);
|
||||
console.log('Usage:', simpleResult.usage);
|
||||
console.log();
|
||||
|
||||
// Use ollama provider via alias
|
||||
const ollamaProvider = createProvider('local/llama2', {
|
||||
baseUrl: 'http://localhost:11434'
|
||||
} as any);
|
||||
console.log(`Created provider: ${ollamaProvider.providerId}/${ollamaProvider.modelId}`);
|
||||
console.log();
|
||||
|
||||
// Demonstrate error handling for unknown provider
|
||||
console.log('=== Error Handling ===\n');
|
||||
try {
|
||||
createProvider('unknown-provider/model');
|
||||
} catch (error: any) {
|
||||
console.log('Expected error:', error.message);
|
||||
}
|
||||
|
||||
console.log('\n=== Example Complete ===');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -1,11 +1,23 @@
|
||||
/**
|
||||
* Providers command - List available LLM providers
|
||||
* Providers command - List and manage LLM providers
|
||||
*
|
||||
* Supports:
|
||||
* - list: List all registered providers
|
||||
* - register: Register a custom provider (programmatic only)
|
||||
* - info: Show provider details
|
||||
*/
|
||||
|
||||
import { getAvailableProviders } from '../../llm/providers';
|
||||
import {
|
||||
getAvailableProviders,
|
||||
listProviders,
|
||||
hasProvider,
|
||||
getDefaultRegistry,
|
||||
isProviderAvailable
|
||||
} from '../../llm/providers';
|
||||
import { hasApiKey } from '../runtime/env';
|
||||
import { outputJson, formatSuccess } from '../output/json';
|
||||
import { outputJson, formatSuccess, formatError } from '../output/json';
|
||||
import * as pretty from '../output/pretty';
|
||||
import { ERROR_CODES } from '../output/errors';
|
||||
|
||||
export interface ProvidersOptions {
|
||||
verbose?: boolean;
|
||||
@@ -15,11 +27,15 @@ export interface ProvidersOptions {
|
||||
|
||||
interface ProviderInfo {
|
||||
name: string;
|
||||
registered: boolean;
|
||||
available: boolean;
|
||||
hasApiKey: boolean;
|
||||
isBuiltin: boolean;
|
||||
models?: string[];
|
||||
}
|
||||
|
||||
const BUILTIN_PROVIDERS = ['openai', 'anthropic', 'google', 'gemini', 'oai', 'claude'];
|
||||
|
||||
const PROVIDER_MODELS: Record<string, string[]> = {
|
||||
openai: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-4', 'gpt-3.5-turbo'],
|
||||
anthropic: ['claude-sonnet-4-20250514', 'claude-3-5-sonnet-20241022', 'claude-3-opus-20240229', 'claude-3-haiku-20240307'],
|
||||
@@ -27,58 +43,128 @@ const PROVIDER_MODELS: Record<string, string[]> = {
|
||||
};
|
||||
|
||||
export async function execute(args: string[], options: ProvidersOptions): Promise<void> {
|
||||
const subcommand = args[0] || 'list';
|
||||
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
|
||||
|
||||
switch (subcommand) {
|
||||
case 'list':
|
||||
await listProvidersCommand(options, outputFormat);
|
||||
break;
|
||||
case 'info':
|
||||
await providerInfoCommand(args[1], options, outputFormat);
|
||||
break;
|
||||
default:
|
||||
await listProvidersCommand(options, outputFormat);
|
||||
}
|
||||
}
|
||||
|
||||
async function listProvidersCommand(options: ProvidersOptions, outputFormat: string): Promise<void> {
|
||||
const registry = getDefaultRegistry();
|
||||
const registeredProviders = registry.list();
|
||||
|
||||
const availableProviders = getAvailableProviders();
|
||||
|
||||
const providers: ProviderInfo[] = [
|
||||
{
|
||||
name: 'openai',
|
||||
available: availableProviders.includes('openai'),
|
||||
hasApiKey: hasApiKey('openai'),
|
||||
models: PROVIDER_MODELS.openai
|
||||
},
|
||||
{
|
||||
name: 'anthropic',
|
||||
available: availableProviders.includes('anthropic'),
|
||||
hasApiKey: hasApiKey('anthropic'),
|
||||
models: PROVIDER_MODELS.anthropic
|
||||
},
|
||||
{
|
||||
name: 'google',
|
||||
available: availableProviders.includes('google'),
|
||||
hasApiKey: hasApiKey('google'),
|
||||
models: PROVIDER_MODELS.google
|
||||
}
|
||||
];
|
||||
const providers: ProviderInfo[] = registeredProviders.map(name => {
|
||||
const isBuiltin = BUILTIN_PROVIDERS.includes(name);
|
||||
return {
|
||||
name,
|
||||
registered: true,
|
||||
available: isProviderAvailable(name),
|
||||
hasApiKey: hasApiKey(name),
|
||||
isBuiltin,
|
||||
models: PROVIDER_MODELS[name]
|
||||
};
|
||||
});
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatSuccess({
|
||||
providers: providers.map(p => ({
|
||||
name: p.name,
|
||||
registered: p.registered,
|
||||
available: p.available,
|
||||
has_api_key: p.hasApiKey,
|
||||
is_builtin: p.isBuiltin,
|
||||
models: options.verbose ? p.models : undefined
|
||||
}))
|
||||
})),
|
||||
total: providers.length,
|
||||
builtin_count: providers.filter(p => p.isBuiltin).length,
|
||||
custom_count: providers.filter(p => !p.isBuiltin).length
|
||||
}));
|
||||
} else {
|
||||
await pretty.heading('Available Providers');
|
||||
await pretty.heading('Registered LLM Providers');
|
||||
|
||||
for (const provider of providers) {
|
||||
const status = provider.available ? '✅' : '❌';
|
||||
const keyStatus = provider.hasApiKey ? '🔑' : '⚠️ No API key';
|
||||
|
||||
await pretty.plain(` ${status} ${provider.name} ${keyStatus}`);
|
||||
|
||||
if (options.verbose && provider.models) {
|
||||
for (const model of provider.models) {
|
||||
await pretty.dim(` - ${model}`);
|
||||
// Built-in providers
|
||||
const builtinProviders = providers.filter(p => p.isBuiltin);
|
||||
if (builtinProviders.length > 0) {
|
||||
await pretty.plain('\n Built-in:');
|
||||
for (const provider of builtinProviders) {
|
||||
const status = provider.available ? '✅' : '❌';
|
||||
const keyStatus = provider.hasApiKey ? '🔑' : '⚠️ No API key';
|
||||
await pretty.plain(` ${status} ${provider.name} ${keyStatus}`);
|
||||
|
||||
if (options.verbose && provider.models) {
|
||||
for (const model of provider.models) {
|
||||
await pretty.dim(` - ${model}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Custom providers
|
||||
const customProviders = providers.filter(p => !p.isBuiltin);
|
||||
if (customProviders.length > 0) {
|
||||
await pretty.plain('\n Custom:');
|
||||
for (const provider of customProviders) {
|
||||
const status = provider.available ? '✅' : '❌';
|
||||
await pretty.plain(` ${status} ${provider.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
await pretty.newline();
|
||||
await pretty.info('Register custom providers with registerProvider():');
|
||||
await pretty.dim(' import { registerProvider } from "praisonai";');
|
||||
await pretty.dim(' registerProvider("cloudflare", CloudflareProvider);');
|
||||
await pretty.newline();
|
||||
await pretty.info('Set API keys via environment variables:');
|
||||
await pretty.dim(' OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY');
|
||||
}
|
||||
}
|
||||
|
||||
async function providerInfoCommand(name: string | undefined, options: ProvidersOptions, outputFormat: string): Promise<void> {
|
||||
if (!name) {
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Provider name required'));
|
||||
} else {
|
||||
await pretty.error('Provider name required. Usage: providers info <name>');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const registry = getDefaultRegistry();
|
||||
const isRegistered = registry.has(name);
|
||||
const isBuiltin = BUILTIN_PROVIDERS.includes(name.toLowerCase());
|
||||
const available = isProviderAvailable(name);
|
||||
const models = PROVIDER_MODELS[name.toLowerCase()];
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatSuccess({
|
||||
name,
|
||||
registered: isRegistered,
|
||||
available,
|
||||
is_builtin: isBuiltin,
|
||||
has_api_key: hasApiKey(name),
|
||||
models: models || []
|
||||
}));
|
||||
} else {
|
||||
await pretty.heading(`Provider: ${name}`);
|
||||
await pretty.plain(` Registered: ${isRegistered ? '✅ Yes' : '❌ No'}`);
|
||||
await pretty.plain(` Available: ${available ? '✅ Yes' : '❌ No'}`);
|
||||
await pretty.plain(` Type: ${isBuiltin ? 'Built-in' : 'Custom'}`);
|
||||
await pretty.plain(` API Key: ${hasApiKey(name) ? '🔑 Set' : '⚠️ Not set'}`);
|
||||
|
||||
if (models && models.length > 0) {
|
||||
await pretty.plain('\n Models:');
|
||||
for (const model of models) {
|
||||
await pretty.dim(` - ${model}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,16 +29,22 @@ export function loadEnvConfig(): EnvConfig {
|
||||
return config;
|
||||
}
|
||||
|
||||
export function hasApiKey(provider: 'openai' | 'anthropic' | 'google'): boolean {
|
||||
switch (provider) {
|
||||
export function hasApiKey(provider: string): boolean {
|
||||
const normalizedProvider = provider.toLowerCase();
|
||||
switch (normalizedProvider) {
|
||||
case 'openai':
|
||||
case 'oai':
|
||||
return !!process.env[ENV_VARS.OPENAI_API_KEY];
|
||||
case 'anthropic':
|
||||
case 'claude':
|
||||
return !!process.env[ENV_VARS.ANTHROPIC_API_KEY];
|
||||
case 'google':
|
||||
case 'gemini':
|
||||
return !!process.env[ENV_VARS.GOOGLE_API_KEY];
|
||||
default:
|
||||
return false;
|
||||
// For custom providers, check for provider-specific env var
|
||||
const envVarName = `${normalizedProvider.toUpperCase()}_API_KEY`;
|
||||
return !!process.env[envVarName];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -225,18 +225,36 @@ export {
|
||||
|
||||
// Export providers with explicit names to avoid conflicts
|
||||
export {
|
||||
// Provider factory and utilities
|
||||
createProvider,
|
||||
getDefaultProvider,
|
||||
parseModelString,
|
||||
isProviderAvailable,
|
||||
getAvailableProviders,
|
||||
// Provider classes
|
||||
OpenAIProvider,
|
||||
AnthropicProvider,
|
||||
GoogleProvider,
|
||||
BaseProvider,
|
||||
// Provider registry (extensibility API)
|
||||
ProviderRegistry,
|
||||
registerProvider,
|
||||
unregisterProvider,
|
||||
hasProvider,
|
||||
listProviders,
|
||||
getDefaultRegistry,
|
||||
createProviderRegistry,
|
||||
registerBuiltinProviders,
|
||||
// Types
|
||||
type LLMProvider,
|
||||
type ProviderConfig,
|
||||
type ProviderFactory,
|
||||
type ProviderConstructor,
|
||||
type ProviderLoader,
|
||||
type RegisterOptions,
|
||||
type IProviderRegistry,
|
||||
type ProviderInput,
|
||||
type CreateProviderOptions,
|
||||
type GenerateTextOptions,
|
||||
type GenerateTextResult,
|
||||
type StreamTextOptions,
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
/**
|
||||
* LLM Providers - Factory and exports for multi-provider support
|
||||
*
|
||||
* This module provides an extensible provider registry system that allows
|
||||
* users to register custom providers (e.g., Cloudflare, Ollama) alongside
|
||||
* the built-in providers (OpenAI, Anthropic, Google).
|
||||
*
|
||||
* @example Register a custom provider
|
||||
* ```typescript
|
||||
* import { registerProvider, Agent } from 'praisonai';
|
||||
* import { CloudflareProvider } from './my-cloudflare-provider';
|
||||
*
|
||||
* registerProvider('cloudflare', CloudflareProvider);
|
||||
* const agent = new Agent({ llm: 'cloudflare/workers-ai' });
|
||||
* ```
|
||||
*/
|
||||
|
||||
export * from './types';
|
||||
@@ -8,20 +21,25 @@ export { OpenAIProvider } from './openai';
|
||||
export { AnthropicProvider } from './anthropic';
|
||||
export { GoogleProvider } from './google';
|
||||
|
||||
import type { LLMProvider, ProviderConfig } from './types';
|
||||
import { OpenAIProvider } from './openai';
|
||||
import { AnthropicProvider } from './anthropic';
|
||||
import { GoogleProvider } from './google';
|
||||
// Export registry types and functions
|
||||
export {
|
||||
ProviderRegistry,
|
||||
ProviderConstructor,
|
||||
ProviderLoader,
|
||||
RegisterOptions,
|
||||
IProviderRegistry,
|
||||
getDefaultRegistry,
|
||||
createProviderRegistry,
|
||||
registerProvider,
|
||||
unregisterProvider,
|
||||
hasProvider,
|
||||
listProviders,
|
||||
registerBuiltinProviders,
|
||||
} from './registry';
|
||||
|
||||
/**
|
||||
* Provider registry for dynamic provider loading
|
||||
*/
|
||||
const PROVIDER_MAP: Record<string, new (modelId: string, config?: ProviderConfig) => LLMProvider> = {
|
||||
openai: OpenAIProvider,
|
||||
anthropic: AnthropicProvider,
|
||||
google: GoogleProvider,
|
||||
gemini: GoogleProvider, // Alias
|
||||
};
|
||||
import type { LLMProvider, ProviderConfig } from './types';
|
||||
import { getDefaultRegistry } from './registry';
|
||||
import type { ProviderConstructor, ProviderRegistry } from './registry';
|
||||
|
||||
/**
|
||||
* Parse model string into provider and model ID
|
||||
@@ -51,25 +69,99 @@ export function parseModelString(model: string): { providerId: string; modelId:
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a provider instance from a model string
|
||||
* Input types for createProvider
|
||||
*/
|
||||
export type ProviderInput =
|
||||
| string // "provider/model" or "model"
|
||||
| LLMProvider // Already instantiated provider
|
||||
| { name: string; modelId?: string; config?: ProviderConfig }; // Spec object
|
||||
|
||||
/**
|
||||
* Options for createProvider
|
||||
*/
|
||||
export interface CreateProviderOptions {
|
||||
/** Override the default registry */
|
||||
registry?: ProviderRegistry;
|
||||
/** Provider configuration */
|
||||
config?: ProviderConfig;
|
||||
/** Model ID (used when input is a constructor or spec without modelId) */
|
||||
modelId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a provider instance from various input types
|
||||
*
|
||||
* @example
|
||||
* @example String input (backward compatible)
|
||||
* ```typescript
|
||||
* const provider = createProvider('openai/gpt-4o');
|
||||
* const provider = createProvider('anthropic/claude-3-5-sonnet-latest');
|
||||
* const provider = createProvider('google/gemini-2.0-flash');
|
||||
* const provider = createProvider('gpt-4o-mini'); // Defaults to OpenAI
|
||||
* ```
|
||||
*
|
||||
* @example Custom provider (after registration)
|
||||
* ```typescript
|
||||
* registerProvider('cloudflare', CloudflareProvider);
|
||||
* const provider = createProvider('cloudflare/workers-ai');
|
||||
* ```
|
||||
*
|
||||
* @example Provider instance (pass-through)
|
||||
* ```typescript
|
||||
* const myProvider = new CustomProvider('model');
|
||||
* const provider = createProvider(myProvider); // Returns same instance
|
||||
* ```
|
||||
*
|
||||
* @example Spec object
|
||||
* ```typescript
|
||||
* const provider = createProvider({ name: 'openai', modelId: 'gpt-4o', config: { timeout: 5000 } });
|
||||
* ```
|
||||
*/
|
||||
export function createProvider(model: string, config?: ProviderConfig): LLMProvider {
|
||||
const { providerId, modelId } = parseModelString(model);
|
||||
|
||||
const ProviderClass = PROVIDER_MAP[providerId];
|
||||
if (!ProviderClass) {
|
||||
throw new Error(`Unknown provider: ${providerId}. Available providers: ${Object.keys(PROVIDER_MAP).join(', ')}`);
|
||||
export function createProvider(
|
||||
input: ProviderInput,
|
||||
options?: CreateProviderOptions | ProviderConfig
|
||||
): LLMProvider {
|
||||
// Handle legacy signature: createProvider(model, config)
|
||||
const opts: CreateProviderOptions = options && 'registry' in options
|
||||
? options
|
||||
: { config: options as ProviderConfig | undefined };
|
||||
|
||||
const registry = opts.registry || getDefaultRegistry();
|
||||
|
||||
// Case 1: Already a provider instance - pass through
|
||||
if (isProviderInstance(input)) {
|
||||
return input;
|
||||
}
|
||||
|
||||
return new ProviderClass(modelId, config);
|
||||
|
||||
// Case 2: Spec object
|
||||
if (typeof input === 'object' && 'name' in input) {
|
||||
const { name, modelId, config } = input;
|
||||
return registry.resolve(name, modelId || 'default', config || opts.config);
|
||||
}
|
||||
|
||||
// Case 3: String - parse and resolve
|
||||
if (typeof input === 'string') {
|
||||
const { providerId, modelId } = parseModelString(input);
|
||||
return registry.resolve(providerId, modelId, opts.config);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Invalid provider input. Expected string, provider instance, or spec object. ` +
|
||||
`Got: ${typeof input}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if value is a provider instance
|
||||
*/
|
||||
function isProviderInstance(value: unknown): value is LLMProvider {
|
||||
return (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
'providerId' in value &&
|
||||
'modelId' in value &&
|
||||
'generateText' in value &&
|
||||
typeof (value as any).generateText === 'function'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,22 +175,34 @@ export function getDefaultProvider(config?: ProviderConfig): LLMProvider {
|
||||
* Check if a provider is available (has required API key)
|
||||
*/
|
||||
export function isProviderAvailable(providerId: string): boolean {
|
||||
switch (providerId.toLowerCase()) {
|
||||
const normalizedId = providerId.toLowerCase();
|
||||
|
||||
// Check if provider is registered
|
||||
if (!getDefaultRegistry().has(normalizedId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for API keys based on known providers
|
||||
switch (normalizedId) {
|
||||
case 'openai':
|
||||
case 'oai':
|
||||
return !!process.env.OPENAI_API_KEY;
|
||||
case 'anthropic':
|
||||
case 'claude':
|
||||
return !!process.env.ANTHROPIC_API_KEY;
|
||||
case 'google':
|
||||
case 'gemini':
|
||||
return !!process.env.GOOGLE_API_KEY;
|
||||
default:
|
||||
return false;
|
||||
// For custom providers, assume available if registered
|
||||
// Users can override this behavior
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of available providers
|
||||
* Get list of available providers (registered and with API keys)
|
||||
*/
|
||||
export function getAvailableProviders(): string[] {
|
||||
return Object.keys(PROVIDER_MAP).filter(isProviderAvailable);
|
||||
return getDefaultRegistry().list().filter(isProviderAvailable);
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
/**
|
||||
* Provider Registry - Extensible provider registration system
|
||||
*
|
||||
* Fixes Issue #1095: Allows users to register custom providers
|
||||
* that can be resolved by name via createProvider().
|
||||
*/
|
||||
|
||||
import type { LLMProvider, ProviderConfig } from './types';
|
||||
|
||||
/**
|
||||
* Provider constructor type
|
||||
*/
|
||||
export type ProviderConstructor = new (modelId: string, config?: ProviderConfig) => LLMProvider;
|
||||
|
||||
/**
|
||||
* Lazy loader function that returns a provider constructor
|
||||
* Used for tree-shaking and lazy loading of provider implementations
|
||||
*/
|
||||
export type ProviderLoader = () => ProviderConstructor;
|
||||
|
||||
/**
|
||||
* Options for registering a provider
|
||||
*/
|
||||
export interface RegisterOptions {
|
||||
/** Allow overwriting an existing registration */
|
||||
override?: boolean;
|
||||
/** Additional names that resolve to this provider */
|
||||
aliases?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal entry in the registry
|
||||
*/
|
||||
interface RegistryEntry {
|
||||
/** The provider constructor or loader */
|
||||
provider: ProviderConstructor | ProviderLoader;
|
||||
/** Whether this is a loader function (vs direct constructor) */
|
||||
isLoader: boolean;
|
||||
/** Cached constructor if loaded from a loader */
|
||||
cachedConstructor?: ProviderConstructor;
|
||||
/** The canonical name (for aliases) */
|
||||
canonicalName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider Registry Interface
|
||||
*/
|
||||
export interface IProviderRegistry {
|
||||
register(name: string, provider: ProviderConstructor | ProviderLoader, options?: RegisterOptions): void;
|
||||
unregister(name: string): boolean;
|
||||
has(name: string): boolean;
|
||||
list(): string[];
|
||||
resolve(name: string, modelId: string, config?: ProviderConfig): LLMProvider;
|
||||
get(name: string): ProviderConstructor | ProviderLoader | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider Registry Implementation
|
||||
*
|
||||
* Manages registration and resolution of LLM providers by name.
|
||||
* Supports lazy loading, aliases, and isolated instances.
|
||||
*/
|
||||
export class ProviderRegistry implements IProviderRegistry {
|
||||
private entries: Map<string, RegistryEntry> = new Map();
|
||||
private aliases: Map<string, string> = new Map(); // alias -> canonical name
|
||||
|
||||
/**
|
||||
* Register a provider by name
|
||||
*
|
||||
* @param name - Provider name (e.g., 'cloudflare', 'ollama')
|
||||
* @param provider - Provider constructor or lazy loader function
|
||||
* @param options - Registration options
|
||||
* @throws Error if name is already registered (unless override: true)
|
||||
*/
|
||||
register(
|
||||
name: string,
|
||||
provider: ProviderConstructor | ProviderLoader,
|
||||
options: RegisterOptions = {}
|
||||
): void {
|
||||
const normalizedName = name.toLowerCase();
|
||||
|
||||
// Check for existing registration
|
||||
if (this.entries.has(normalizedName) && !options.override) {
|
||||
throw new Error(
|
||||
`Provider '${name}' is already registered. ` +
|
||||
`Use { override: true } to replace it.`
|
||||
);
|
||||
}
|
||||
|
||||
// Determine if this is a loader function or direct constructor
|
||||
const isLoader = this.isLoaderFunction(provider);
|
||||
|
||||
const entry: RegistryEntry = {
|
||||
provider,
|
||||
isLoader,
|
||||
canonicalName: normalizedName,
|
||||
};
|
||||
|
||||
this.entries.set(normalizedName, entry);
|
||||
|
||||
// Register aliases
|
||||
if (options.aliases) {
|
||||
for (const alias of options.aliases) {
|
||||
const normalizedAlias = alias.toLowerCase();
|
||||
if (this.entries.has(normalizedAlias) && !options.override) {
|
||||
throw new Error(
|
||||
`Alias '${alias}' conflicts with existing provider. ` +
|
||||
`Use { override: true } to replace it.`
|
||||
);
|
||||
}
|
||||
this.aliases.set(normalizedAlias, normalizedName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a provider by name
|
||||
*
|
||||
* @param name - Provider name to unregister
|
||||
* @returns true if provider was unregistered, false if not found
|
||||
*/
|
||||
unregister(name: string): boolean {
|
||||
const normalizedName = name.toLowerCase();
|
||||
|
||||
// Check if it's an alias
|
||||
if (this.aliases.has(normalizedName)) {
|
||||
this.aliases.delete(normalizedName);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if it's a canonical name
|
||||
if (this.entries.has(normalizedName)) {
|
||||
// Remove all aliases pointing to this provider
|
||||
for (const [alias, canonical] of this.aliases.entries()) {
|
||||
if (canonical === normalizedName) {
|
||||
this.aliases.delete(alias);
|
||||
}
|
||||
}
|
||||
this.entries.delete(normalizedName);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a provider is registered
|
||||
*
|
||||
* @param name - Provider name to check
|
||||
* @returns true if provider is registered
|
||||
*/
|
||||
has(name: string): boolean {
|
||||
const normalizedName = name.toLowerCase();
|
||||
return this.entries.has(normalizedName) || this.aliases.has(normalizedName);
|
||||
}
|
||||
|
||||
/**
|
||||
* List all registered provider names (canonical names only)
|
||||
*
|
||||
* @returns Array of provider names
|
||||
*/
|
||||
list(): string[] {
|
||||
return Array.from(this.entries.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* List all names including aliases
|
||||
*
|
||||
* @returns Array of all registered names and aliases
|
||||
*/
|
||||
listAll(): string[] {
|
||||
return [...this.entries.keys(), ...this.aliases.keys()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a provider by name, creating an instance
|
||||
*
|
||||
* @param name - Provider name
|
||||
* @param modelId - Model ID to pass to constructor
|
||||
* @param config - Optional provider config
|
||||
* @returns Provider instance
|
||||
* @throws Error if provider not found
|
||||
*/
|
||||
resolve(name: string, modelId: string, config?: ProviderConfig): LLMProvider {
|
||||
const normalizedName = name.toLowerCase();
|
||||
|
||||
// Resolve alias to canonical name
|
||||
const canonicalName = this.aliases.get(normalizedName) || normalizedName;
|
||||
|
||||
const entry = this.entries.get(canonicalName);
|
||||
if (!entry) {
|
||||
const available = this.list();
|
||||
throw new Error(
|
||||
`Unknown provider: '${name}'. ` +
|
||||
`Available providers: ${available.length > 0 ? available.join(', ') : 'none'}. ` +
|
||||
`Register a custom provider with registerProvider('${name}', YourProviderClass).`
|
||||
);
|
||||
}
|
||||
|
||||
// Get constructor (resolve loader if needed)
|
||||
const Constructor = this.getConstructor(entry);
|
||||
|
||||
return new Constructor(modelId, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the provider constructor/loader without instantiating
|
||||
*
|
||||
* @param name - Provider name
|
||||
* @returns Provider constructor/loader or undefined
|
||||
*/
|
||||
get(name: string): ProviderConstructor | ProviderLoader | undefined {
|
||||
const normalizedName = name.toLowerCase();
|
||||
const canonicalName = this.aliases.get(normalizedName) || normalizedName;
|
||||
return this.entries.get(canonicalName)?.provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resolved constructor for an entry
|
||||
*/
|
||||
private getConstructor(entry: RegistryEntry): ProviderConstructor {
|
||||
if (!entry.isLoader) {
|
||||
return entry.provider as ProviderConstructor;
|
||||
}
|
||||
|
||||
// Lazy load and cache
|
||||
if (!entry.cachedConstructor) {
|
||||
const loader = entry.provider as ProviderLoader;
|
||||
entry.cachedConstructor = loader();
|
||||
}
|
||||
|
||||
return entry.cachedConstructor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a value is a loader function vs a constructor
|
||||
* Loaders are arrow functions or regular functions that return a class
|
||||
* Constructors have a prototype with constructor
|
||||
*/
|
||||
private isLoaderFunction(value: ProviderConstructor | ProviderLoader): boolean {
|
||||
// If it has a prototype with more than just constructor, it's likely a class
|
||||
if (value.prototype && Object.getOwnPropertyNames(value.prototype).length > 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if it looks like a class (has 'prototype' that is an object with constructor)
|
||||
if (value.prototype && value.prototype.constructor === value) {
|
||||
// Additional check: classes typically have a name and their toString starts with 'class'
|
||||
const str = value.toString();
|
||||
if (str.startsWith('class ') || str.includes('function ')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Arrow functions and simple functions are loaders
|
||||
return typeof value === 'function' && !value.prototype?.constructor;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Default Registry Singleton
|
||||
// ============================================================================
|
||||
|
||||
let defaultRegistry: ProviderRegistry | null = null;
|
||||
|
||||
/**
|
||||
* Get the default global provider registry
|
||||
*
|
||||
* This is the registry used by createProvider() when no custom registry is specified.
|
||||
* Built-in providers (OpenAI, Anthropic, Google) are registered here.
|
||||
*/
|
||||
export function getDefaultRegistry(): ProviderRegistry {
|
||||
if (!defaultRegistry) {
|
||||
defaultRegistry = new ProviderRegistry();
|
||||
registerBuiltinProviders(defaultRegistry);
|
||||
}
|
||||
return defaultRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new isolated provider registry
|
||||
*
|
||||
* Use this when you need a separate registry that doesn't share
|
||||
* registrations with the default global registry.
|
||||
*/
|
||||
export function createProviderRegistry(): ProviderRegistry {
|
||||
return new ProviderRegistry();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a provider to the default global registry
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { registerProvider } from 'praisonai';
|
||||
* import { CloudflareProvider } from './my-cloudflare-provider';
|
||||
*
|
||||
* registerProvider('cloudflare', CloudflareProvider);
|
||||
*
|
||||
* // Now works:
|
||||
* const agent = new Agent({ llm: 'cloudflare/workers-ai' });
|
||||
* ```
|
||||
*/
|
||||
export function registerProvider(
|
||||
name: string,
|
||||
provider: ProviderConstructor | ProviderLoader,
|
||||
options?: RegisterOptions
|
||||
): void {
|
||||
getDefaultRegistry().register(name, provider, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a provider from the default global registry
|
||||
*/
|
||||
export function unregisterProvider(name: string): boolean {
|
||||
return getDefaultRegistry().unregister(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a provider is registered in the default registry
|
||||
*/
|
||||
export function hasProvider(name: string): boolean {
|
||||
return getDefaultRegistry().has(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* List all providers in the default registry
|
||||
*/
|
||||
export function listProviders(): string[] {
|
||||
return getDefaultRegistry().list();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Built-in Provider Registration
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Register built-in providers to a registry
|
||||
* Uses lazy loaders to avoid importing all providers at module load time
|
||||
*/
|
||||
export function registerBuiltinProviders(registry: ProviderRegistry): void {
|
||||
// OpenAI - lazy loaded
|
||||
registry.register('openai', () => {
|
||||
const { OpenAIProvider } = require('./openai');
|
||||
return OpenAIProvider;
|
||||
}, { aliases: ['oai'] });
|
||||
|
||||
// Anthropic - lazy loaded
|
||||
registry.register('anthropic', () => {
|
||||
const { AnthropicProvider } = require('./anthropic');
|
||||
return AnthropicProvider;
|
||||
}, { aliases: ['claude'] });
|
||||
|
||||
// Google - lazy loaded
|
||||
registry.register('google', () => {
|
||||
const { GoogleProvider } = require('./google');
|
||||
return GoogleProvider;
|
||||
}, { aliases: ['gemini'] });
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the default registry (mainly for testing)
|
||||
* @internal
|
||||
*/
|
||||
export function _resetDefaultRegistry(): void {
|
||||
defaultRegistry = null;
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
/**
|
||||
* Provider Registry Unit Tests (TDD)
|
||||
*
|
||||
* Tests for the extensible provider registry that fixes Issue #1095
|
||||
* These tests define the expected behavior before implementation.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals';
|
||||
import type { LLMProvider, ProviderConfig, GenerateTextOptions, GenerateTextResult, StreamTextOptions, StreamChunk, GenerateObjectOptions, GenerateObjectResult } from '../../../src/llm/providers/types';
|
||||
|
||||
// Mock provider for testing
|
||||
class MockProvider implements LLMProvider {
|
||||
readonly providerId: string;
|
||||
readonly modelId: string;
|
||||
|
||||
constructor(modelId: string, config?: ProviderConfig) {
|
||||
this.providerId = 'mock';
|
||||
this.modelId = modelId;
|
||||
}
|
||||
|
||||
async generateText(options: GenerateTextOptions): Promise<GenerateTextResult> {
|
||||
return {
|
||||
text: 'mock response',
|
||||
usage: { promptTokens: 10, completionTokens: 20, totalTokens: 30 },
|
||||
finishReason: 'stop'
|
||||
};
|
||||
}
|
||||
|
||||
async streamText(options: StreamTextOptions): Promise<AsyncIterable<StreamChunk>> {
|
||||
return {
|
||||
async *[Symbol.asyncIterator]() {
|
||||
yield { text: 'mock' };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async generateObject<T>(options: GenerateObjectOptions<T>): Promise<GenerateObjectResult<T>> {
|
||||
return {
|
||||
object: {} as T,
|
||||
usage: { promptTokens: 10, completionTokens: 20, totalTokens: 30 }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Custom provider for testing registration
|
||||
class CustomCloudflareProvider implements LLMProvider {
|
||||
readonly providerId = 'cloudflare';
|
||||
readonly modelId: string;
|
||||
|
||||
constructor(modelId: string, config?: ProviderConfig) {
|
||||
this.modelId = modelId;
|
||||
}
|
||||
|
||||
async generateText(options: GenerateTextOptions): Promise<GenerateTextResult> {
|
||||
return {
|
||||
text: 'cloudflare response',
|
||||
usage: { promptTokens: 5, completionTokens: 15, totalTokens: 20 },
|
||||
finishReason: 'stop'
|
||||
};
|
||||
}
|
||||
|
||||
async streamText(options: StreamTextOptions): Promise<AsyncIterable<StreamChunk>> {
|
||||
return {
|
||||
async *[Symbol.asyncIterator]() {
|
||||
yield { text: 'cloudflare' };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async generateObject<T>(options: GenerateObjectOptions<T>): Promise<GenerateObjectResult<T>> {
|
||||
return {
|
||||
object: {} as T,
|
||||
usage: { promptTokens: 5, completionTokens: 15, totalTokens: 20 }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
describe('ProviderRegistry', () => {
|
||||
// Import will be from the implementation once created
|
||||
let ProviderRegistry: any;
|
||||
let createProviderRegistry: any;
|
||||
let getDefaultRegistry: any;
|
||||
let registerProvider: any;
|
||||
let unregisterProvider: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Dynamic import to get the registry module
|
||||
const registryModule = await import('../../../src/llm/providers/registry');
|
||||
ProviderRegistry = registryModule.ProviderRegistry;
|
||||
createProviderRegistry = registryModule.createProviderRegistry;
|
||||
getDefaultRegistry = registryModule.getDefaultRegistry;
|
||||
registerProvider = registryModule.registerProvider;
|
||||
unregisterProvider = registryModule.unregisterProvider;
|
||||
});
|
||||
|
||||
describe('Registry Creation', () => {
|
||||
it('should create an empty registry', () => {
|
||||
const registry = createProviderRegistry();
|
||||
expect(registry).toBeDefined();
|
||||
expect(registry.list()).toEqual([]);
|
||||
});
|
||||
|
||||
it('should get the default registry singleton', () => {
|
||||
const registry1 = getDefaultRegistry();
|
||||
const registry2 = getDefaultRegistry();
|
||||
expect(registry1).toBe(registry2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Provider Registration', () => {
|
||||
it('should register a provider class', () => {
|
||||
const registry = createProviderRegistry();
|
||||
registry.register('mock', MockProvider);
|
||||
expect(registry.has('mock')).toBe(true);
|
||||
expect(registry.list()).toContain('mock');
|
||||
});
|
||||
|
||||
it('should register a provider with lazy loader function', () => {
|
||||
const registry = createProviderRegistry();
|
||||
const loader = () => MockProvider;
|
||||
registry.register('mock', loader);
|
||||
expect(registry.has('mock')).toBe(true);
|
||||
});
|
||||
|
||||
it('should register provider with aliases', () => {
|
||||
const registry = createProviderRegistry();
|
||||
registry.register('cloudflare', CustomCloudflareProvider, { aliases: ['cf', 'workers-ai'] });
|
||||
|
||||
expect(registry.has('cloudflare')).toBe(true);
|
||||
expect(registry.has('cf')).toBe(true);
|
||||
expect(registry.has('workers-ai')).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw on duplicate registration by default', () => {
|
||||
const registry = createProviderRegistry();
|
||||
registry.register('mock', MockProvider);
|
||||
|
||||
expect(() => {
|
||||
registry.register('mock', CustomCloudflareProvider);
|
||||
}).toThrow(/already registered/i);
|
||||
});
|
||||
|
||||
it('should allow override with explicit flag', () => {
|
||||
const registry = createProviderRegistry();
|
||||
registry.register('mock', MockProvider);
|
||||
registry.register('mock', CustomCloudflareProvider, { override: true });
|
||||
|
||||
const provider = registry.resolve('mock', 'test-model');
|
||||
expect(provider.providerId).toBe('cloudflare');
|
||||
});
|
||||
|
||||
it('should unregister a provider', () => {
|
||||
const registry = createProviderRegistry();
|
||||
registry.register('mock', MockProvider);
|
||||
expect(registry.has('mock')).toBe(true);
|
||||
|
||||
const result = registry.unregister('mock');
|
||||
expect(result).toBe(true);
|
||||
expect(registry.has('mock')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when unregistering non-existent provider', () => {
|
||||
const registry = createProviderRegistry();
|
||||
const result = registry.unregister('nonexistent');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Provider Resolution', () => {
|
||||
it('should resolve a registered provider by name', () => {
|
||||
const registry = createProviderRegistry();
|
||||
registry.register('mock', MockProvider);
|
||||
|
||||
const provider = registry.resolve('mock', 'test-model');
|
||||
expect(provider).toBeInstanceOf(MockProvider);
|
||||
expect(provider.modelId).toBe('test-model');
|
||||
});
|
||||
|
||||
it('should resolve provider with config', () => {
|
||||
const registry = createProviderRegistry();
|
||||
registry.register('mock', MockProvider);
|
||||
|
||||
const config: ProviderConfig = { apiKey: 'test-key', timeout: 5000 };
|
||||
const provider = registry.resolve('mock', 'test-model', config);
|
||||
expect(provider).toBeInstanceOf(MockProvider);
|
||||
});
|
||||
|
||||
it('should resolve provider from lazy loader', () => {
|
||||
const registry = createProviderRegistry();
|
||||
const loaderFn = jest.fn(() => MockProvider);
|
||||
registry.register('mock', loaderFn);
|
||||
|
||||
// First resolution should call loader
|
||||
const provider1 = registry.resolve('mock', 'model-1');
|
||||
expect(loaderFn).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Second resolution should reuse cached constructor
|
||||
const provider2 = registry.resolve('mock', 'model-2');
|
||||
expect(loaderFn).toHaveBeenCalledTimes(1); // Still 1, not called again
|
||||
});
|
||||
|
||||
it('should throw clear error for unknown provider', () => {
|
||||
const registry = createProviderRegistry();
|
||||
registry.register('openai', MockProvider);
|
||||
registry.register('anthropic', MockProvider);
|
||||
|
||||
expect(() => {
|
||||
registry.resolve('cloudflare', 'model');
|
||||
}).toThrow(/unknown provider.*cloudflare/i);
|
||||
|
||||
// Error should include available providers
|
||||
try {
|
||||
registry.resolve('cloudflare', 'model');
|
||||
} catch (e: any) {
|
||||
expect(e.message).toMatch(/openai/i);
|
||||
expect(e.message).toMatch(/anthropic/i);
|
||||
}
|
||||
});
|
||||
|
||||
it('should resolve via alias', () => {
|
||||
const registry = createProviderRegistry();
|
||||
registry.register('cloudflare', CustomCloudflareProvider, { aliases: ['cf'] });
|
||||
|
||||
const provider = registry.resolve('cf', 'workers-ai-model');
|
||||
expect(provider.providerId).toBe('cloudflare');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Global Registration Functions', () => {
|
||||
it('should register provider to default registry', () => {
|
||||
// Reset default registry state
|
||||
const registry = getDefaultRegistry();
|
||||
|
||||
registerProvider('custom-test', MockProvider);
|
||||
expect(registry.has('custom-test')).toBe(true);
|
||||
|
||||
// Cleanup
|
||||
unregisterProvider('custom-test');
|
||||
});
|
||||
|
||||
it('should unregister provider from default registry', () => {
|
||||
registerProvider('custom-test-2', MockProvider);
|
||||
expect(getDefaultRegistry().has('custom-test-2')).toBe(true);
|
||||
|
||||
unregisterProvider('custom-test-2');
|
||||
expect(getDefaultRegistry().has('custom-test-2')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multi-agent Safety', () => {
|
||||
it('should support isolated registries per context', () => {
|
||||
const registry1 = createProviderRegistry();
|
||||
const registry2 = createProviderRegistry();
|
||||
|
||||
registry1.register('provider-a', MockProvider);
|
||||
registry2.register('provider-b', CustomCloudflareProvider);
|
||||
|
||||
expect(registry1.has('provider-a')).toBe(true);
|
||||
expect(registry1.has('provider-b')).toBe(false);
|
||||
|
||||
expect(registry2.has('provider-a')).toBe(false);
|
||||
expect(registry2.has('provider-b')).toBe(true);
|
||||
});
|
||||
|
||||
it('should not leak registrations between isolated registries', () => {
|
||||
const registry1 = createProviderRegistry();
|
||||
const registry2 = createProviderRegistry();
|
||||
|
||||
registry1.register('shared-name', MockProvider);
|
||||
registry2.register('shared-name', CustomCloudflareProvider);
|
||||
|
||||
const provider1 = registry1.resolve('shared-name', 'model');
|
||||
const provider2 = registry2.resolve('shared-name', 'model');
|
||||
|
||||
expect(provider1.providerId).toBe('mock');
|
||||
expect(provider2.providerId).toBe('cloudflare');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createProvider with Registry', () => {
|
||||
let createProvider: any;
|
||||
let registerProvider: any;
|
||||
let unregisterProvider: any;
|
||||
let getDefaultRegistry: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
const providersModule = await import('../../../src/llm/providers');
|
||||
createProvider = providersModule.createProvider;
|
||||
registerProvider = providersModule.registerProvider;
|
||||
unregisterProvider = providersModule.unregisterProvider;
|
||||
getDefaultRegistry = providersModule.getDefaultRegistry;
|
||||
});
|
||||
|
||||
describe('Backward Compatibility', () => {
|
||||
it('should resolve built-in openai provider', () => {
|
||||
if (!process.env.OPENAI_API_KEY) {
|
||||
console.log('Skipping: OPENAI_API_KEY not set');
|
||||
return;
|
||||
}
|
||||
const provider = createProvider('openai/gpt-4o-mini');
|
||||
expect(provider.providerId).toBe('openai');
|
||||
expect(provider.modelId).toBe('gpt-4o-mini');
|
||||
});
|
||||
|
||||
it('should resolve built-in anthropic provider', () => {
|
||||
if (!process.env.ANTHROPIC_API_KEY) {
|
||||
console.log('Skipping: ANTHROPIC_API_KEY not set');
|
||||
return;
|
||||
}
|
||||
const provider = createProvider('anthropic/claude-3-5-sonnet-latest');
|
||||
expect(provider.providerId).toBe('anthropic');
|
||||
});
|
||||
|
||||
it('should resolve built-in google provider', () => {
|
||||
if (!process.env.GOOGLE_API_KEY) {
|
||||
console.log('Skipping: GOOGLE_API_KEY not set');
|
||||
return;
|
||||
}
|
||||
const provider = createProvider('google/gemini-2.0-flash');
|
||||
expect(provider.providerId).toBe('google');
|
||||
});
|
||||
|
||||
it('should default to openai for gpt- models', () => {
|
||||
if (!process.env.OPENAI_API_KEY) {
|
||||
console.log('Skipping: OPENAI_API_KEY not set');
|
||||
return;
|
||||
}
|
||||
const provider = createProvider('gpt-4o-mini');
|
||||
expect(provider.providerId).toBe('openai');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom Provider Support', () => {
|
||||
afterEach(() => {
|
||||
// Cleanup custom registrations
|
||||
try {
|
||||
unregisterProvider('custom-test');
|
||||
} catch {}
|
||||
});
|
||||
|
||||
it('should resolve custom registered provider', () => {
|
||||
registerProvider('custom-test', MockProvider);
|
||||
|
||||
const provider = createProvider('custom-test/my-model');
|
||||
expect(provider.providerId).toBe('mock');
|
||||
expect(provider.modelId).toBe('my-model');
|
||||
});
|
||||
|
||||
it('should accept provider instance directly', () => {
|
||||
const instance = new MockProvider('direct-model');
|
||||
const provider = createProvider(instance);
|
||||
|
||||
expect(provider).toBe(instance);
|
||||
expect(provider.modelId).toBe('direct-model');
|
||||
});
|
||||
|
||||
it('should accept provider constructor', () => {
|
||||
const provider = createProvider(MockProvider, { modelId: 'constructor-model' });
|
||||
expect(provider).toBeInstanceOf(MockProvider);
|
||||
});
|
||||
|
||||
it('should accept provider spec object', () => {
|
||||
registerProvider('custom-test', MockProvider);
|
||||
|
||||
const provider = createProvider({
|
||||
name: 'custom-test',
|
||||
modelId: 'spec-model',
|
||||
config: { timeout: 5000 }
|
||||
});
|
||||
expect(provider.providerId).toBe('mock');
|
||||
expect(provider.modelId).toBe('spec-model');
|
||||
});
|
||||
|
||||
it('should use custom registry when provided', () => {
|
||||
const { createProviderRegistry } = require('../../../src/llm/providers/registry');
|
||||
const customRegistry = createProviderRegistry();
|
||||
customRegistry.register('isolated', CustomCloudflareProvider);
|
||||
|
||||
const provider = createProvider('isolated/model', { registry: customRegistry });
|
||||
expect(provider.providerId).toBe('cloudflare');
|
||||
|
||||
// Should not be in default registry
|
||||
expect(() => createProvider('isolated/model')).toThrow(/unknown provider/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Messages', () => {
|
||||
it('should provide helpful error for unknown provider', () => {
|
||||
expect(() => createProvider('nonexistent/model')).toThrow();
|
||||
|
||||
try {
|
||||
createProvider('nonexistent/model');
|
||||
} catch (e: any) {
|
||||
expect(e.message).toMatch(/unknown provider/i);
|
||||
expect(e.message).toMatch(/nonexistent/i);
|
||||
expect(e.message).toMatch(/available/i);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,8 +3,8 @@ class Praisonai < Formula
|
||||
|
||||
desc "AI tools for various AI applications"
|
||||
homepage "https://github.com/MervinPraison/PraisonAI"
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.7.8.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.7.8.tar.gz | shasum -a 256`.split.first
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.7.9.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.7.9.tar.gz | shasum -a 256`.split.first
|
||||
license "MIT"
|
||||
|
||||
depends_on "[email protected]"
|
||||
|
||||
@@ -57,7 +57,7 @@ class CloudDeployer:
|
||||
file.write("FROM python:3.11-slim\n")
|
||||
file.write("WORKDIR /app\n")
|
||||
file.write("COPY . .\n")
|
||||
file.write("RUN pip install flask praisonai==2.7.8 gunicorn markdown\n")
|
||||
file.write("RUN pip install flask praisonai==2.7.9 gunicorn markdown\n")
|
||||
file.write("EXPOSE 8080\n")
|
||||
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
"""
|
||||
LLM Module for PraisonAI CLI Wrapper.
|
||||
|
||||
This module provides LLM provider registry and utilities for the CLI wrapper.
|
||||
The core LLM functionality is in praisonaiagents.llm.
|
||||
"""
|
||||
|
||||
# Lazy loading for performance
|
||||
_lazy_cache = {}
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
"""Lazy load LLM registry classes."""
|
||||
if name in _lazy_cache:
|
||||
return _lazy_cache[name]
|
||||
|
||||
if name == "LLMProviderRegistry":
|
||||
from .registry import LLMProviderRegistry
|
||||
_lazy_cache[name] = LLMProviderRegistry
|
||||
return LLMProviderRegistry
|
||||
elif name == "get_default_llm_registry":
|
||||
from .registry import get_default_llm_registry
|
||||
_lazy_cache[name] = get_default_llm_registry
|
||||
return get_default_llm_registry
|
||||
elif name == "register_llm_provider":
|
||||
from .registry import register_llm_provider
|
||||
_lazy_cache[name] = register_llm_provider
|
||||
return register_llm_provider
|
||||
elif name == "unregister_llm_provider":
|
||||
from .registry import unregister_llm_provider
|
||||
_lazy_cache[name] = unregister_llm_provider
|
||||
return unregister_llm_provider
|
||||
elif name == "has_llm_provider":
|
||||
from .registry import has_llm_provider
|
||||
_lazy_cache[name] = has_llm_provider
|
||||
return has_llm_provider
|
||||
elif name == "list_llm_providers":
|
||||
from .registry import list_llm_providers
|
||||
_lazy_cache[name] = list_llm_providers
|
||||
return list_llm_providers
|
||||
elif name == "create_llm_provider":
|
||||
from .registry import create_llm_provider
|
||||
_lazy_cache[name] = create_llm_provider
|
||||
return create_llm_provider
|
||||
elif name == "parse_model_string":
|
||||
from .registry import parse_model_string
|
||||
_lazy_cache[name] = parse_model_string
|
||||
return parse_model_string
|
||||
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
|
||||
__all__ = [
|
||||
"LLMProviderRegistry",
|
||||
"get_default_llm_registry",
|
||||
"register_llm_provider",
|
||||
"unregister_llm_provider",
|
||||
"has_llm_provider",
|
||||
"list_llm_providers",
|
||||
"create_llm_provider",
|
||||
"parse_model_string",
|
||||
]
|
||||
@@ -0,0 +1,374 @@
|
||||
"""
|
||||
LLM Provider Registry - Extensible provider registration system for Python.
|
||||
|
||||
This module provides parity with the TypeScript ProviderRegistry,
|
||||
allowing users to register custom LLM providers that can be resolved by name.
|
||||
|
||||
Example:
|
||||
from praisonai.llm import register_llm_provider, create_llm_provider
|
||||
|
||||
class CloudflareProvider:
|
||||
provider_id = "cloudflare"
|
||||
def __init__(self, model_id, config=None):
|
||||
self.model_id = model_id
|
||||
self.config = config or {}
|
||||
def generate(self, prompt):
|
||||
# Implementation
|
||||
pass
|
||||
|
||||
register_llm_provider("cloudflare", CloudflareProvider)
|
||||
provider = create_llm_provider("cloudflare/workers-ai")
|
||||
"""
|
||||
|
||||
from typing import Any, Callable, Dict, List, Optional, Type, Union
|
||||
|
||||
|
||||
# Type aliases
|
||||
ProviderClass = Type[Any] # Class with __init__(model_id, config)
|
||||
ProviderFactory = Callable[[str, Optional[Dict[str, Any]]], Any]
|
||||
ProviderType = Union[ProviderClass, ProviderFactory]
|
||||
|
||||
|
||||
class LLMProviderRegistry:
|
||||
"""
|
||||
Registry for LLM providers.
|
||||
|
||||
Manages registration and resolution of LLM providers by name.
|
||||
Supports lazy loading, aliases, and isolated instances.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize an empty registry."""
|
||||
self._providers: Dict[str, ProviderType] = {}
|
||||
self._aliases: Dict[str, str] = {} # alias -> canonical name
|
||||
|
||||
def register(
|
||||
self,
|
||||
name: str,
|
||||
provider: ProviderType,
|
||||
*,
|
||||
override: bool = False,
|
||||
aliases: Optional[List[str]] = None
|
||||
) -> None:
|
||||
"""
|
||||
Register a provider by name.
|
||||
|
||||
Args:
|
||||
name: Provider name (e.g., 'cloudflare', 'ollama')
|
||||
provider: Provider class or factory function
|
||||
override: Allow overwriting existing registration
|
||||
aliases: Additional names that resolve to this provider
|
||||
|
||||
Raises:
|
||||
ValueError: If name is already registered (unless override=True)
|
||||
"""
|
||||
normalized_name = name.lower()
|
||||
|
||||
# Check for existing registration
|
||||
if normalized_name in self._providers and not override:
|
||||
raise ValueError(
|
||||
f"Provider '{name}' is already registered. "
|
||||
f"Use override=True to replace it."
|
||||
)
|
||||
|
||||
self._providers[normalized_name] = provider
|
||||
|
||||
# Register aliases
|
||||
if aliases:
|
||||
for alias in aliases:
|
||||
normalized_alias = alias.lower()
|
||||
if normalized_alias in self._providers and not override:
|
||||
raise ValueError(
|
||||
f"Alias '{alias}' conflicts with existing provider. "
|
||||
f"Use override=True to replace it."
|
||||
)
|
||||
self._aliases[normalized_alias] = normalized_name
|
||||
|
||||
def unregister(self, name: str) -> bool:
|
||||
"""
|
||||
Unregister a provider by name.
|
||||
|
||||
Args:
|
||||
name: Provider name to unregister
|
||||
|
||||
Returns:
|
||||
True if provider was unregistered, False if not found
|
||||
"""
|
||||
normalized_name = name.lower()
|
||||
|
||||
# Check if it's an alias
|
||||
if normalized_name in self._aliases:
|
||||
del self._aliases[normalized_name]
|
||||
return True
|
||||
|
||||
# Check if it's a canonical name
|
||||
if normalized_name in self._providers:
|
||||
# Remove all aliases pointing to this provider
|
||||
aliases_to_remove = [
|
||||
alias for alias, canonical in self._aliases.items()
|
||||
if canonical == normalized_name
|
||||
]
|
||||
for alias in aliases_to_remove:
|
||||
del self._aliases[alias]
|
||||
|
||||
del self._providers[normalized_name]
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def has(self, name: str) -> bool:
|
||||
"""
|
||||
Check if a provider is registered.
|
||||
|
||||
Args:
|
||||
name: Provider name to check
|
||||
|
||||
Returns:
|
||||
True if provider is registered
|
||||
"""
|
||||
normalized_name = name.lower()
|
||||
return normalized_name in self._providers or normalized_name in self._aliases
|
||||
|
||||
def list(self) -> List[str]:
|
||||
"""
|
||||
List all registered provider names (canonical names only).
|
||||
|
||||
Returns:
|
||||
List of provider names
|
||||
"""
|
||||
return list(self._providers.keys())
|
||||
|
||||
def list_all(self) -> List[str]:
|
||||
"""
|
||||
List all names including aliases.
|
||||
|
||||
Returns:
|
||||
List of all registered names and aliases
|
||||
"""
|
||||
return list(self._providers.keys()) + list(self._aliases.keys())
|
||||
|
||||
def resolve(
|
||||
self,
|
||||
name: str,
|
||||
model_id: str,
|
||||
config: Optional[Dict[str, Any]] = None
|
||||
) -> Any:
|
||||
"""
|
||||
Resolve a provider by name, creating an instance.
|
||||
|
||||
Args:
|
||||
name: Provider name
|
||||
model_id: Model ID to pass to constructor
|
||||
config: Optional provider config
|
||||
|
||||
Returns:
|
||||
Provider instance
|
||||
|
||||
Raises:
|
||||
ValueError: If provider not found
|
||||
"""
|
||||
normalized_name = name.lower()
|
||||
|
||||
# Resolve alias to canonical name
|
||||
canonical_name = self._aliases.get(normalized_name, normalized_name)
|
||||
|
||||
provider = self._providers.get(canonical_name)
|
||||
if provider is None:
|
||||
available = self.list()
|
||||
raise ValueError(
|
||||
f"Unknown provider: '{name}'. "
|
||||
f"Available providers: {', '.join(available) if available else 'none'}. "
|
||||
f"Register a custom provider with register_llm_provider('{name}', YourProviderClass)."
|
||||
)
|
||||
|
||||
# Create instance
|
||||
return provider(model_id, config)
|
||||
|
||||
def get(self, name: str) -> Optional[ProviderType]:
|
||||
"""
|
||||
Get the provider class/factory without instantiating.
|
||||
|
||||
Args:
|
||||
name: Provider name
|
||||
|
||||
Returns:
|
||||
Provider class/factory or None
|
||||
"""
|
||||
normalized_name = name.lower()
|
||||
canonical_name = self._aliases.get(normalized_name, normalized_name)
|
||||
return self._providers.get(canonical_name)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Default Registry Singleton
|
||||
# ============================================================================
|
||||
|
||||
_default_registry: Optional[LLMProviderRegistry] = None
|
||||
|
||||
|
||||
def get_default_llm_registry() -> LLMProviderRegistry:
|
||||
"""
|
||||
Get the default global LLM provider registry.
|
||||
|
||||
This is the registry used by create_llm_provider() when no custom registry
|
||||
is specified.
|
||||
"""
|
||||
global _default_registry
|
||||
if _default_registry is None:
|
||||
_default_registry = LLMProviderRegistry()
|
||||
_register_builtin_providers(_default_registry)
|
||||
return _default_registry
|
||||
|
||||
|
||||
def _register_builtin_providers(registry: LLMProviderRegistry) -> None:
|
||||
"""
|
||||
Register built-in providers to a registry.
|
||||
|
||||
Uses lazy loading to avoid importing heavy dependencies at module load time.
|
||||
"""
|
||||
# Note: In Python, the actual LLM providers are in praisonaiagents.llm
|
||||
# which uses LiteLLM. This registry is for custom provider extensions.
|
||||
# Built-in providers are handled by LiteLLM automatically.
|
||||
pass
|
||||
|
||||
|
||||
def register_llm_provider(
|
||||
name: str,
|
||||
provider: ProviderType,
|
||||
*,
|
||||
override: bool = False,
|
||||
aliases: Optional[List[str]] = None
|
||||
) -> None:
|
||||
"""
|
||||
Register a provider to the default global registry.
|
||||
|
||||
Example:
|
||||
from praisonai.llm import register_llm_provider
|
||||
|
||||
class CloudflareProvider:
|
||||
provider_id = "cloudflare"
|
||||
def __init__(self, model_id, config=None):
|
||||
self.model_id = model_id
|
||||
self.config = config or {}
|
||||
|
||||
register_llm_provider("cloudflare", CloudflareProvider)
|
||||
"""
|
||||
get_default_llm_registry().register(name, provider, override=override, aliases=aliases)
|
||||
|
||||
|
||||
def unregister_llm_provider(name: str) -> bool:
|
||||
"""Unregister a provider from the default global registry."""
|
||||
return get_default_llm_registry().unregister(name)
|
||||
|
||||
|
||||
def has_llm_provider(name: str) -> bool:
|
||||
"""Check if a provider is registered in the default registry."""
|
||||
return get_default_llm_registry().has(name)
|
||||
|
||||
|
||||
def list_llm_providers() -> List[str]:
|
||||
"""List all providers in the default registry."""
|
||||
return get_default_llm_registry().list()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Provider Creation
|
||||
# ============================================================================
|
||||
|
||||
def parse_model_string(model: str) -> Dict[str, str]:
|
||||
"""
|
||||
Parse model string into provider and model ID.
|
||||
|
||||
Supports formats:
|
||||
- "provider/model" (e.g., "openai/gpt-4o")
|
||||
- "model" (defaults based on prefix)
|
||||
|
||||
Args:
|
||||
model: Model string
|
||||
|
||||
Returns:
|
||||
Dict with 'provider_id' and 'model_id' keys
|
||||
"""
|
||||
if "/" in model:
|
||||
parts = model.split("/", 1)
|
||||
return {"provider_id": parts[0].lower(), "model_id": parts[1]}
|
||||
|
||||
# Default based on model prefix
|
||||
model_lower = model.lower()
|
||||
if model_lower.startswith("gpt-") or model_lower.startswith("o1") or model_lower.startswith("o3"):
|
||||
return {"provider_id": "openai", "model_id": model}
|
||||
if model_lower.startswith("claude-"):
|
||||
return {"provider_id": "anthropic", "model_id": model}
|
||||
if model_lower.startswith("gemini-"):
|
||||
return {"provider_id": "google", "model_id": model}
|
||||
|
||||
# Default to openai
|
||||
return {"provider_id": "openai", "model_id": model}
|
||||
|
||||
|
||||
def create_llm_provider(
|
||||
input_value: Union[str, Dict[str, Any], Any],
|
||||
*,
|
||||
registry: Optional[LLMProviderRegistry] = None,
|
||||
config: Optional[Dict[str, Any]] = None
|
||||
) -> Any:
|
||||
"""
|
||||
Create a provider instance from various input types.
|
||||
|
||||
Args:
|
||||
input_value: One of:
|
||||
- String: "provider/model" or "model"
|
||||
- Dict: {"name": "provider", "model_id": "model", "config": {...}}
|
||||
- Provider instance: passed through
|
||||
registry: Optional custom registry (defaults to global)
|
||||
config: Optional provider config
|
||||
|
||||
Returns:
|
||||
Provider instance
|
||||
|
||||
Example:
|
||||
# String input
|
||||
provider = create_llm_provider("openai/gpt-4o")
|
||||
|
||||
# Custom provider (after registration)
|
||||
register_llm_provider("cloudflare", CloudflareProvider)
|
||||
provider = create_llm_provider("cloudflare/workers-ai")
|
||||
|
||||
# Provider instance (pass-through)
|
||||
provider = create_llm_provider(existing_provider)
|
||||
|
||||
# Spec dict
|
||||
provider = create_llm_provider({
|
||||
"name": "openai",
|
||||
"model_id": "gpt-4o",
|
||||
"config": {"timeout": 5000}
|
||||
})
|
||||
"""
|
||||
reg = registry or get_default_llm_registry()
|
||||
|
||||
# Case 1: Already a provider instance (has provider_id and model_id)
|
||||
if hasattr(input_value, "provider_id") and hasattr(input_value, "model_id"):
|
||||
return input_value
|
||||
|
||||
# Case 2: Spec dict
|
||||
if isinstance(input_value, dict) and "name" in input_value:
|
||||
name = input_value["name"]
|
||||
model_id = input_value.get("model_id", "default")
|
||||
provider_config = input_value.get("config") or config
|
||||
return reg.resolve(name, model_id, provider_config)
|
||||
|
||||
# Case 3: String - parse and resolve
|
||||
if isinstance(input_value, str):
|
||||
parsed = parse_model_string(input_value)
|
||||
return reg.resolve(parsed["provider_id"], parsed["model_id"], config)
|
||||
|
||||
raise ValueError(
|
||||
f"Invalid provider input. Expected string, provider instance, or spec dict. "
|
||||
f"Got: {type(input_value).__name__}"
|
||||
)
|
||||
|
||||
|
||||
def _reset_default_registry() -> None:
|
||||
"""Reset the default registry (mainly for testing)."""
|
||||
global _default_registry
|
||||
_default_registry = None
|
||||
@@ -1 +1 @@
|
||||
__version__ = "2.7.8"
|
||||
__version__ = "2.7.9"
|
||||
@@ -0,0 +1,310 @@
|
||||
"""
|
||||
LLM Provider Registry Unit Tests (TDD)
|
||||
|
||||
Tests for the extensible LLM provider registry that provides parity
|
||||
with the TypeScript implementation for Issue #1095.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
|
||||
class MockLLMProvider:
|
||||
"""Mock LLM provider for testing."""
|
||||
|
||||
provider_id = "mock"
|
||||
|
||||
def __init__(self, model_id: str, config: Optional[Dict[str, Any]] = None):
|
||||
self.model_id = model_id
|
||||
self.config = config or {}
|
||||
|
||||
def generate(self, prompt: str) -> str:
|
||||
return f"Mock response for: {prompt}"
|
||||
|
||||
|
||||
class CustomCloudflareProvider:
|
||||
"""Custom provider for testing registration."""
|
||||
|
||||
provider_id = "cloudflare"
|
||||
|
||||
def __init__(self, model_id: str, config: Optional[Dict[str, Any]] = None):
|
||||
self.model_id = model_id
|
||||
self.config = config or {}
|
||||
|
||||
def generate(self, prompt: str) -> str:
|
||||
return f"Cloudflare response for: {prompt}"
|
||||
|
||||
|
||||
class TestLLMProviderRegistry:
|
||||
"""Tests for LLMProviderRegistry class."""
|
||||
|
||||
def test_create_empty_registry(self):
|
||||
"""Should create an empty registry."""
|
||||
from praisonai.llm.registry import LLMProviderRegistry
|
||||
|
||||
registry = LLMProviderRegistry()
|
||||
assert registry is not None
|
||||
assert registry.list() == []
|
||||
|
||||
def test_register_provider_class(self):
|
||||
"""Should register a provider class."""
|
||||
from praisonai.llm.registry import LLMProviderRegistry
|
||||
|
||||
registry = LLMProviderRegistry()
|
||||
registry.register("mock", MockLLMProvider)
|
||||
|
||||
assert registry.has("mock")
|
||||
assert "mock" in registry.list()
|
||||
|
||||
def test_register_provider_with_factory(self):
|
||||
"""Should register a provider with a factory function."""
|
||||
from praisonai.llm.registry import LLMProviderRegistry
|
||||
|
||||
registry = LLMProviderRegistry()
|
||||
def factory(model_id, config=None):
|
||||
return MockLLMProvider(model_id, config)
|
||||
registry.register("mock", factory)
|
||||
|
||||
assert registry.has("mock")
|
||||
|
||||
def test_register_provider_with_aliases(self):
|
||||
"""Should register provider with aliases."""
|
||||
from praisonai.llm.registry import LLMProviderRegistry
|
||||
|
||||
registry = LLMProviderRegistry()
|
||||
registry.register("cloudflare", CustomCloudflareProvider, aliases=["cf", "workers-ai"])
|
||||
|
||||
assert registry.has("cloudflare")
|
||||
assert registry.has("cf")
|
||||
assert registry.has("workers-ai")
|
||||
|
||||
def test_duplicate_registration_raises_error(self):
|
||||
"""Should raise error on duplicate registration by default."""
|
||||
from praisonai.llm.registry import LLMProviderRegistry
|
||||
|
||||
registry = LLMProviderRegistry()
|
||||
registry.register("mock", MockLLMProvider)
|
||||
|
||||
with pytest.raises(ValueError, match="already registered"):
|
||||
registry.register("mock", CustomCloudflareProvider)
|
||||
|
||||
def test_override_registration(self):
|
||||
"""Should allow override with explicit flag."""
|
||||
from praisonai.llm.registry import LLMProviderRegistry
|
||||
|
||||
registry = LLMProviderRegistry()
|
||||
registry.register("mock", MockLLMProvider)
|
||||
registry.register("mock", CustomCloudflareProvider, override=True)
|
||||
|
||||
provider = registry.resolve("mock", "test-model")
|
||||
assert provider.provider_id == "cloudflare"
|
||||
|
||||
def test_unregister_provider(self):
|
||||
"""Should unregister a provider."""
|
||||
from praisonai.llm.registry import LLMProviderRegistry
|
||||
|
||||
registry = LLMProviderRegistry()
|
||||
registry.register("mock", MockLLMProvider)
|
||||
assert registry.has("mock")
|
||||
|
||||
result = registry.unregister("mock")
|
||||
assert result is True
|
||||
assert not registry.has("mock")
|
||||
|
||||
def test_unregister_nonexistent_returns_false(self):
|
||||
"""Should return False when unregistering non-existent provider."""
|
||||
from praisonai.llm.registry import LLMProviderRegistry
|
||||
|
||||
registry = LLMProviderRegistry()
|
||||
result = registry.unregister("nonexistent")
|
||||
assert result is False
|
||||
|
||||
def test_resolve_provider(self):
|
||||
"""Should resolve a registered provider by name."""
|
||||
from praisonai.llm.registry import LLMProviderRegistry
|
||||
|
||||
registry = LLMProviderRegistry()
|
||||
registry.register("mock", MockLLMProvider)
|
||||
|
||||
provider = registry.resolve("mock", "test-model")
|
||||
assert isinstance(provider, MockLLMProvider)
|
||||
assert provider.model_id == "test-model"
|
||||
|
||||
def test_resolve_provider_with_config(self):
|
||||
"""Should resolve provider with config."""
|
||||
from praisonai.llm.registry import LLMProviderRegistry
|
||||
|
||||
registry = LLMProviderRegistry()
|
||||
registry.register("mock", MockLLMProvider)
|
||||
|
||||
config = {"api_key": "test-key", "timeout": 5000}
|
||||
provider = registry.resolve("mock", "test-model", config)
|
||||
assert provider.config == config
|
||||
|
||||
def test_resolve_via_alias(self):
|
||||
"""Should resolve provider via alias."""
|
||||
from praisonai.llm.registry import LLMProviderRegistry
|
||||
|
||||
registry = LLMProviderRegistry()
|
||||
registry.register("cloudflare", CustomCloudflareProvider, aliases=["cf"])
|
||||
|
||||
provider = registry.resolve("cf", "workers-ai-model")
|
||||
assert provider.provider_id == "cloudflare"
|
||||
|
||||
def test_resolve_unknown_provider_raises_error(self):
|
||||
"""Should raise clear error for unknown provider."""
|
||||
from praisonai.llm.registry import LLMProviderRegistry
|
||||
|
||||
registry = LLMProviderRegistry()
|
||||
registry.register("openai", MockLLMProvider)
|
||||
registry.register("anthropic", MockLLMProvider)
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
registry.resolve("cloudflare", "model")
|
||||
|
||||
error_msg = str(exc_info.value).lower()
|
||||
assert "unknown provider" in error_msg
|
||||
assert "cloudflare" in error_msg
|
||||
assert "openai" in error_msg or "anthropic" in error_msg
|
||||
|
||||
|
||||
class TestDefaultRegistry:
|
||||
"""Tests for default global registry."""
|
||||
|
||||
def test_get_default_registry_singleton(self):
|
||||
"""Should return the same registry instance."""
|
||||
from praisonai.llm.registry import get_default_llm_registry
|
||||
|
||||
registry1 = get_default_llm_registry()
|
||||
registry2 = get_default_llm_registry()
|
||||
assert registry1 is registry2
|
||||
|
||||
def test_register_provider_global(self):
|
||||
"""Should register provider to default registry."""
|
||||
from praisonai.llm.registry import (
|
||||
register_llm_provider,
|
||||
unregister_llm_provider,
|
||||
get_default_llm_registry
|
||||
)
|
||||
|
||||
register_llm_provider("custom-test", MockLLMProvider)
|
||||
assert get_default_llm_registry().has("custom-test")
|
||||
|
||||
# Cleanup
|
||||
unregister_llm_provider("custom-test")
|
||||
|
||||
def test_unregister_provider_global(self):
|
||||
"""Should unregister provider from default registry."""
|
||||
from praisonai.llm.registry import (
|
||||
register_llm_provider,
|
||||
unregister_llm_provider,
|
||||
get_default_llm_registry
|
||||
)
|
||||
|
||||
register_llm_provider("custom-test-2", MockLLMProvider)
|
||||
assert get_default_llm_registry().has("custom-test-2")
|
||||
|
||||
unregister_llm_provider("custom-test-2")
|
||||
assert not get_default_llm_registry().has("custom-test-2")
|
||||
|
||||
|
||||
class TestMultiAgentSafety:
|
||||
"""Tests for multi-agent safety with isolated registries."""
|
||||
|
||||
def test_isolated_registries(self):
|
||||
"""Should support isolated registries per context."""
|
||||
from praisonai.llm.registry import LLMProviderRegistry
|
||||
|
||||
registry1 = LLMProviderRegistry()
|
||||
registry2 = LLMProviderRegistry()
|
||||
|
||||
registry1.register("provider-a", MockLLMProvider)
|
||||
registry2.register("provider-b", CustomCloudflareProvider)
|
||||
|
||||
assert registry1.has("provider-a")
|
||||
assert not registry1.has("provider-b")
|
||||
|
||||
assert not registry2.has("provider-a")
|
||||
assert registry2.has("provider-b")
|
||||
|
||||
def test_no_leak_between_registries(self):
|
||||
"""Should not leak registrations between isolated registries."""
|
||||
from praisonai.llm.registry import LLMProviderRegistry
|
||||
|
||||
registry1 = LLMProviderRegistry()
|
||||
registry2 = LLMProviderRegistry()
|
||||
|
||||
registry1.register("shared-name", MockLLMProvider)
|
||||
registry2.register("shared-name", CustomCloudflareProvider)
|
||||
|
||||
provider1 = registry1.resolve("shared-name", "model")
|
||||
provider2 = registry2.resolve("shared-name", "model")
|
||||
|
||||
assert provider1.provider_id == "mock"
|
||||
assert provider2.provider_id == "cloudflare"
|
||||
|
||||
|
||||
class TestCreateLLMProvider:
|
||||
"""Tests for create_llm_provider function."""
|
||||
|
||||
def test_create_from_string(self):
|
||||
"""Should create provider from string."""
|
||||
from praisonai.llm.registry import (
|
||||
create_llm_provider,
|
||||
register_llm_provider,
|
||||
unregister_llm_provider
|
||||
)
|
||||
|
||||
register_llm_provider("mock", MockLLMProvider)
|
||||
|
||||
try:
|
||||
provider = create_llm_provider("mock/test-model")
|
||||
assert provider.provider_id == "mock"
|
||||
assert provider.model_id == "test-model"
|
||||
finally:
|
||||
unregister_llm_provider("mock")
|
||||
|
||||
def test_create_from_instance(self):
|
||||
"""Should pass through provider instance."""
|
||||
from praisonai.llm.registry import create_llm_provider
|
||||
|
||||
instance = MockLLMProvider("direct-model")
|
||||
provider = create_llm_provider(instance)
|
||||
|
||||
assert provider is instance
|
||||
assert provider.model_id == "direct-model"
|
||||
|
||||
def test_create_from_spec_dict(self):
|
||||
"""Should create provider from spec dict."""
|
||||
from praisonai.llm.registry import (
|
||||
create_llm_provider,
|
||||
register_llm_provider,
|
||||
unregister_llm_provider
|
||||
)
|
||||
|
||||
register_llm_provider("mock", MockLLMProvider)
|
||||
|
||||
try:
|
||||
provider = create_llm_provider({
|
||||
"name": "mock",
|
||||
"model_id": "spec-model",
|
||||
"config": {"timeout": 5000}
|
||||
})
|
||||
assert provider.provider_id == "mock"
|
||||
assert provider.model_id == "spec-model"
|
||||
finally:
|
||||
unregister_llm_provider("mock")
|
||||
|
||||
def test_create_with_custom_registry(self):
|
||||
"""Should use custom registry when provided."""
|
||||
from praisonai.llm.registry import create_llm_provider, LLMProviderRegistry
|
||||
|
||||
custom_registry = LLMProviderRegistry()
|
||||
custom_registry.register("isolated", CustomCloudflareProvider)
|
||||
|
||||
provider = create_llm_provider("isolated/model", registry=custom_registry)
|
||||
assert provider.provider_id == "cloudflare"
|
||||
|
||||
# Should not be in default registry
|
||||
with pytest.raises(ValueError):
|
||||
create_llm_provider("isolated/model")
|
||||
Reference in new issue
Block a user