Custom Provider Registry
Register and use custom LLM providers in PraisonAI Python SDK
Custom Provider Registry
The Custom Provider Registry allows you to register custom LLM providers that extend PraisonAI’s capabilities beyond the built-in providers (OpenAI, Anthropic, Google via LiteLLM).
Built-in providers
Built-in providers are automatically registered when a registry is created:
| Provider name | Aliases | Backend |
|---|---|---|
openai | oai | LiteLLM (litellm.completion / litellm.acompletion) |
groq | — | LiteLLM (hosted Groq via groq/… prefix) |
anthropic | claude | LiteLLM |
google | gemini, google_genai | LiteLLM |
cohere | — | LiteLLM (hosted Cohere via cohere/… prefix) |
ollama | — | LiteLLM (local Ollama server) |
ollama_chat | — | LiteLLM (Ollama with tool-calling prefix) |
lm_studio | — | LiteLLM (LM Studio local server) |
vllm | — | LiteLLM (self-hosted vLLM) |
hosted_vllm | — | LiteLLM (OpenAI-compatible vLLM server) |
from praisonaiagents import Agent
# No setup needed - the ollama/ prefix is built-in.
agent = Agent(
name="Local Assistant",
instructions="You run locally via Ollama",
llm="ollama/llama3.2",
)
agent.start("Explain what a local LLM is in one sentence.")from praisonai.llm import create_llm_provider
# Both work out of the box
provider1 = create_llm_provider("openai/gpt-4o")
provider2 = create_llm_provider("oai/gpt-4o")The hosted groq/cohere prefixes (PR #5922) and the five local routes (PR #4800) also build directly — no register_llm_provider() needed:
from praisonai.llm import create_llm_provider
# Hosted — no register_llm_provider() needed (PR #5922).
create_llm_provider("groq/llama-3.3-70b-versatile")
create_llm_provider("cohere/command-r")
# Local — no register_llm_provider() needed (PR #4800).
create_llm_provider("ollama/llama3")
create_llm_provider("ollama_chat/llama3") # tool-calling prefix
create_llm_provider("lm_studio/qwen2.5-7b-instruct")
create_llm_provider("vllm/meta-llama/Llama-3.1-8B-Instruct")
create_llm_provider("hosted_vllm/Qwen2.5-7B") # OpenAI-compatible vLLMUnknown prefixes fail loudly
PraisonAIModel (used by CrewAI and AutoGen YAML) raises ValueError for a provider/model whose prefix is not built-in and not registered, when no base_url, OPENAI_API_BASE, or OPENAI_BASE_URL is set — instead of sending it to api.openai.com. Set an endpoint or call register_llm_provider(...) to fix it. Agent(llm="...") is not affected. See Provider Prefix Validation.
What Problem Does It Solve?
- Integrate proprietary or internal LLM APIs
- Add support for new providers before LiteLLM supports them
- Create mock providers for testing
- Build custom routing or caching layers
Quick Start
- Define provider
python class MyCustomProvider: provider_id = "my-custom" def __init__(self, model_id, config=None): self.model_id = model_id self.config = config or {} def generate(self, prompt): return f"Response from {self.model_id}" async def generate_async(self, prompt): return f"Async response from {self.model_id}" - Register and use
python from praisonai.llm import register_llm_provider, create_llm_provider register_llm_provider("my-custom", MyCustomProvider) provider = create_llm_provider("my-custom/my-model") provider.generate("Hello") # sync await provider.generate_async("Hello") # async
Where a Registered Provider Takes Effect
Registering a provider isn’t just for direct create_llm_provider(...) calls — the registry is now consulted on the hot LLM paths, so your provider is used end-to-end:
PraisonAIModel.get_model()— every framework adapter’s model resolver. Any non-built-in prefix (e.g.bedrock/) resolves through your registered provider.AutoGenerator._completion_impl— the YAML generator’s structured-completion path, before the LiteLLM → OpenAI ladder.
Resolution precedence:
- Built-in prefixes (
openai/,groq/,anthropic/,google/,cohere/,ollama/,openrouter/) keep the fast, direct-SDK path — never routed through the registry. - Non-built-in prefixes are looked up in the registry and, if registered, use your provider.
- Anything else falls through to the LiteLLM → OpenAI ladder.
Structured Outputs from the YAML Generator
To be used by AutoGenerator for structured completion, a registered provider opts in by exposing a sync or async generate_structured(...). Both shapes are accepted — a sync implementation is automatically offloaded to a worker thread so it never parks the caller’s event loop or the shared AsyncBridge loop (PR #6137).
Async
from praisonai.llm import register_llm_provider
class BedrockProvider:
provider_id = "bedrock"
def __init__(self, model_id, config=None):
self.model_id = model_id
self.config = config or {}
async def generate_structured(self, response_model, messages, **kwargs):
# Return an instance of `response_model` (a Pydantic model).
...
register_llm_provider("bedrock", BedrockProvider)
# AutoGenerator now delegates structured completion for "bedrock/..." models
# to your provider, before falling back to LiteLLM/OpenAI.Sync
from praisonai.llm import register_llm_provider
class BedrockProvider:
provider_id = "bedrock"
def __init__(self, model_id, config=None):
self.model_id = model_id
self.config = config or {}
def generate_structured(self, response_model, messages, **kwargs):
# Return an instance of `response_model` (a Pydantic model).
# PraisonAI runs this in a worker thread via asyncio.to_thread.
...
register_llm_provider("bedrock", BedrockProvider)Providers that don’t define generate_structured(...) are skipped and the built-in ladder runs unchanged.
Thread Safety
All registry methods (register, unregister, resolve, alias lookup, and listing) are safe to call concurrently from threads. Built-in providers are registered automatically on first instantiation.
Singleton access pattern (preferred for app code):
from praisonai.llm.registry import LLMProviderRegistry
registry = LLMProviderRegistry.get_instance() # double-checked-locked singletonThe existing helpers register_llm_provider / create_llm_provider continue to work unchanged (backwards compatible).
How Provider Resolution Works
The registry resolves providers in this order:
1. Explicit Provider Format: provider/model
# Explicitly specify provider
provider = create_llm_provider("cloudflare/workers-ai-model")
# Resolves to: provider_id="cloudflare", model_id="workers-ai-model"2. Model Prefix Inference
When no provider is specified, the model name prefix determines the provider:
| Model Prefix | Inferred Provider |
|---|---|
gpt-, o1, o3* | openai |
claude-* | anthropic |
gemini-* | google |
| Other | openai (default) |
# Built-in provider examples:
create_llm_provider("openai/gpt-4o")
create_llm_provider("oai/gpt-4o") # Using alias
# Custom provider (after registration):
register_llm_provider("cloudflare", CloudflareProvider)
create_llm_provider("cloudflare/workers-ai-model")3. Custom Provider with Explicit Name
# After registering "cloudflare"
create_llm_provider("cloudflare/workers-ai")
# Resolves to your registered CloudflareProviderWhere registered providers take effect
Registering a provider makes it reachable from every entry point that resolves models through the wrapper — not just direct create_llm_provider(...) calls.
Two wrapper hot paths now consult the registry before falling back to the built-in ladder.
| Hot path | What it calls on your provider | Opt-in |
|---|---|---|
PraisonAIModel.get_model() — every Agent(llm="myprov/…") call routed through a framework adapter (AutoGen, CrewAI, etc.) | provider.get_client() if defined, otherwise the provider instance itself is returned as the “client” | Passive — any registered provider works |
BaseAutoGenerator._completion_impl — YAML auto-generation and any generator inheriting from BaseAutoGenerator | await provider.generate_structured(response_model, messages, kwargs) if async; await asyncio.to_thread(provider.generate_structured, response_model, messages, kwargs) if sync | Explicit — providers without generate_structured fall through to the built-in LiteLLM ladder |
End-to-end Bedrock example
# 1. Define a Bedrock-shaped provider.
from praisonai.llm import register_llm_provider
class BedrockProvider:
provider_id = "bedrock"
def __init__(self, model_id, config=None):
self.model_id = model_id
self.config = config or {}
import boto3
self._client = boto3.client(
"bedrock-runtime",
region_name=self.config.get("region_name", "us-east-1"),
)
# Framework-adapter hot path (PraisonAIModel.get_model)
def get_client(self):
return self._client
# AutoGenerator hot path (BaseAutoGenerator._completion_impl)
async def generate_structured(self, response_model, messages, **kwargs):
# Call Bedrock, parse into response_model, return the pydantic model instance.
...
register_llm_provider("bedrock", BedrockProvider)
# 2. Now every wrapper entry point resolves through the provider — no OPENAI_API_KEY needed.
from praisonaiagents import Agent
agent = Agent(
name="Claude on Bedrock",
instructions="Answer using Claude on AWS Bedrock.",
llm="bedrock/anthropic.claude-3-sonnet-20240229-v1:0", # <- was previously an OpenAI-key ValueError
)
result = agent.start("Summarise the latest PR in one sentence.")- If
generate_structured(...)is sync (e.g. you use the blockingboto3client directly), that works too — PraisonAI offloads it to a worker thread so the caller’s loop stays responsive (PR #6137).
Provider Aliases
Built-in providers support aliases for shorter, more convenient model specifications:
| Canonical | Aliases |
|---|---|
openai | oai |
anthropic | claude |
google | gemini, google_genai |
Using Aliases
from praisonaiagents import Agent
# These are equivalent ways to use Claude
agent1 = Agent(llm="anthropic/claude-3-5-sonnet")
agent2 = Agent(llm="claude/claude-3-5-sonnet") # Alias
# These are equivalent ways to use OpenAI
agent3 = Agent(llm="openai/gpt-4o")
agent4 = Agent(llm="oai/gpt-4o") # Alias
# These are equivalent ways to use Google
agent5 = Agent(llm="google/gemini-1.5-pro")
agent6 = Agent(llm="gemini/gemini-1.5-pro") # AliasRegistering Custom Providers with Aliases
When registering your own custom providers, you can specify aliases:
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 {}
def generate(self, prompt):
# Implementation
pass
# Register with aliases
register_llm_provider(
"cloudflare",
CloudflareProvider,
aliases=["cf", "workers"]
)
# Now all these work:
provider1 = create_llm_provider("cloudflare/workers-ai")
provider2 = create_llm_provider("cf/workers-ai")
provider3 = create_llm_provider("workers/workers-ai")Registering Custom Providers Safely
Unique Name Rules
Provider names must be unique. Attempting to register a duplicate name raises an error:
register_llm_provider("my-provider", MyProvider)
register_llm_provider("my-provider", AnotherProvider) # Raises ValueError!Error message:
ValueError: Provider 'my-provider' is already registered. Use override=True to replace it.Alias Rules
Aliases provide alternative names for a provider:
register_llm_provider("cloudflare", CloudflareProvider, aliases=["cf", "workers-ai"])
# All of these now work:
create_llm_provider("cloudflare/model")
create_llm_provider("cf/model")
create_llm_provider("workers-ai/model")Alias collision detection:
register_llm_provider("provider-a", ProviderA, aliases=["shared"])
register_llm_provider("provider-b", ProviderB, aliases=["shared"]) # Raises ValueError!Error message:
ValueError: Alias 'shared' is already registered (points to 'provider-a'). Use override=True to replace it.Override Flag
Use override=True only when you intentionally want to replace an existing registration:
# Replace existing provider
register_llm_provider("my-provider", NewProvider, override=True)Avoiding Collisions
Recommended Naming Pattern
Use a namespace prefix to avoid collisions:
# Good: namespaced provider names
register_llm_provider("mycompany-internal-llm", InternalProvider)
register_llm_provider("myproject-mock", MockProvider)
# Avoid: generic names that might conflict
register_llm_provider("custom", CustomProvider) # Too generic
register_llm_provider("llm", LLMProvider) # Too genericReserved Names
These provider names are now registered providers (not just inference defaults):
openai– Auto-registered LiteLLM provideranthropic– Auto-registered LiteLLM providergoogle– Auto-registered LiteLLM providergroq– Auto-registered LiteLLM provider (hosted Groq) — PR #5922cohere– Auto-registered LiteLLM provider (hosted Cohere) — PR #5922ollama– Auto-registered LiteLLM provider (local Ollama server) — PR #4800ollama_chat– Auto-registered LiteLLM provider (Ollama tool-calling prefix) — PR #4800lm_studio– Auto-registered LiteLLM provider (LM Studio) — PR #4800vllm– Auto-registered LiteLLM provider (self-hosted vLLM) — PR #4800hosted_vllm– Auto-registered LiteLLM provider (OpenAI-compatible vLLM) — PR #4800- Their aliases:
oai,claude,gemini,google_genai
Multi-Agent Guidance
Global Registry (Default)
The default global registry is shared across all code:
from praisonai.llm import register_llm_provider
# This affects all code using the default registry
register_llm_provider("shared-provider", SharedProvider)Isolated Registry (Per Agent/Run)
For multi-agent scenarios where you need isolation:
from praisonai.llm import LLMProviderRegistry, create_llm_provider
# Create isolated registries
agent1_registry = LLMProviderRegistry()
agent2_registry = LLMProviderRegistry()
# Register different providers to each
agent1_registry.register("custom", Agent1Provider)
agent2_registry.register("custom", Agent2Provider)
# Use with create_llm_provider
provider1 = create_llm_provider("custom/model", registry=agent1_registry)
provider2 = create_llm_provider("custom/model", registry=agent2_registry)
# provider1 and provider2 are different instances from different classesBenefits of isolated registries:
- No global state mutation
- Safe for concurrent/parallel agent runs
- Each agent can have its own provider configuration
- Testing isolation
API Reference
register_llm_provider
register_llm_provider(
name: str,
provider: ProviderClass | ProviderFactory,
*,
override: bool = False,
aliases: list[str] | None = None
) -> Nonecreate_llm_provider
create_llm_provider(
input_value: str | dict | ProviderInstance,
*,
registry: LLMProviderRegistry | None = None,
config: dict | None = None
) -> ProviderInstanceLLMProviderRegistry
class LLMProviderRegistry:
def register(name, provider, *, override=False, aliases=None) -> None
def unregister(name) -> bool
def has(name) -> bool
def list() -> list[str]
def list_all() -> list[str] # Includes aliases
def resolve(name, model_id, config=None) -> ProviderInstance
def get(name) -> ProviderClass | NoneUtility Functions
from praisonai.llm import (
get_default_llm_registry, # Get global registry
has_llm_provider, # Check if provider exists
list_llm_providers, # List registered providers
unregister_llm_provider, # Remove a provider
parse_model_string, # Parse "provider/model" strings
)Breaking Changes
Removed APIs
LLMProviderRegistry.get_instance()→ Useget_default_llm_registry()LLMProviderRegistry._instanceand_instance_lockclass attributes removedpraisonai.profiler.default_profilermodule attribute removed
Changed Semantics
The LLMProviderRegistry.register() and LLMProviderRegistry.resolve() methods now have PluginRegistry semantics:
# OLD (pre-PR #1675): 3-arg resolve
registry.resolve(name, model_id, config)
# NEW: Use register_provider/resolve_provider instead
registry.register_provider(name, provider, aliases=aliases)
provider_instance = registry.resolve_provider(name, model_id, config)Built-in Names Win Over Entry-Point Plugins
LLMProviderRegistry subclasses the base PluginRegistry, so since PraisonAI PR #4176 a package cannot ship a praisonai.llm_providers entry point named openai, anthropic, or google and expect it to take over. During entry-point discovery, any name whose lowercase form matches a built-in loader is skipped, the built-in is kept, and a DEBUG line notes the collision. Matching is case-insensitive (OpenAI and openai collide).
Runtime registration is the supported override path — it is deliberate dependency injection and still overrides a built-in:
from praisonai.llm.registry import get_default_llm_registry
def _my_openai():
from my_pkg import MyOpenAI
return MyOpenAI
get_default_llm_registry().register("openai", _my_openai, override=True)Preserved Aliases
These methods continue to work for backward compatibility:
has(),list(),list_all()- Module-level
register_llm_provider(...),create_llm_provider(...)
Minimal Example
from praisonai.llm import register_llm_provider, create_llm_provider
class EchoProvider:
"""Simple provider that echoes input."""
provider_id = "echo"
def __init__(self, model_id, config=None):
self.model_id = model_id
self.config = config or {}
def generate(self, prompt):
return f"[{self.model_id}] Echo: {prompt}"
# Register
register_llm_provider("echo", EchoProvider)
# Use
provider = create_llm_provider("echo/v1")
print(provider.generate("Hello!"))
# Output: [v1] Echo: Hello!Troubleshooting
Common Errors
“Provider ‘X’ is already registered”
# Problem: Duplicate registration
register_llm_provider("my-provider", ProviderA)
register_llm_provider("my-provider", ProviderB) # Error!
# Solution 1: Use a different name
register_llm_provider("my-provider-v2", ProviderB)
# Solution 2: Override intentionally
register_llm_provider("my-provider", ProviderB, override=True)“Unknown provider: ‘X'”
# Problem: Provider not registered
provider = create_llm_provider("unregistered/model") # Error!
# Solution: Register first
register_llm_provider("unregistered", MyProvider)
provider = create_llm_provider("unregistered/model")“Alias ‘X’ conflicts with existing provider”
# Problem: Alias matches an existing provider name
register_llm_provider("provider-a", ProviderA)
register_llm_provider("provider-b", ProviderB, aliases=["provider-a"]) # Error!
# Solution: Use a unique alias
register_llm_provider("provider-b", ProviderB, aliases=["alias-b"])Performance Notes
- Lazy imports: The registry module only imports
typing– no heavy dependencies - Import time: ~800μs for
praisonai.llm.registry - O(1) operations:
has(),resolve(), andregister()are all O(1) - No LiteLLM coupling: The registry is completely independent of LiteLLM
Migration note
Before PR #3011, register_llm_provider("bedrock", ...) succeeded but Agent(llm="bedrock/…") raised ValueError: OPENAI_API_KEY environment variable is required for the default OpenAI service. — because the framework-adapter resolver never consulted the registry. If you worked around this by shipping OPENAI_API_KEY=not-needed or a shim adapter, you can now delete the workaround: the registered provider is used directly.
See Also
- Models Overview – Built-in provider configuration
- LLM Gateways – Pre-registered gateway providers
- Advanced Multi-Provider Patterns – Routing across providers
- LiteLLM Models – Providers supported by LiteLLM