Endpoint Provider Registry
Plug custom endpoint providers (A2A, MCP, recipe, agents-api…) into PraisonAI via Python entry points
from praisonaiagents import Agent
agent = Agent(
name="Endpoint Agent",
instructions="Invoke the acme endpoint provider and summarise the result."
)
agent.start("Call the acme-echo endpoint with {'x': 1}")Endpoint Provider Registry lets you plug custom server-side providers (recipe, agents-api, MCP, A2A, openai-compat, and your own) into PraisonAI via a single Python entry-point group — no fork required.
Publish once under praisonai.endpoint_providers. One plugin is then visible to get_provider(), praisonai endpoints types, and praisonai endpoints invoke --type.
export PRAISON_ENDPOINT_URL="http://localhost:8765"
praisonai serve --endpoint mcpfrom praisonai.endpoints.registry import get_provider
provider = get_provider("mcp", base_url="http://localhost:8765")A plugin published once under praisonai.endpoint_providers reaches both the provider registry and the CLI dispatch surface.
Quick Start
- Use a built-in provider
python from praisonai.endpoints.registry import get_provider provider = get_provider("recipe", base_url="http://localhost:8765") - Register at runtime
python from praisonai.endpoints.registry import register_provider from praisonai.endpoints.providers.base import BaseProvider class MyCustomProvider(BaseProvider): def __init__(self, base_url, api_key=None, **kwargs): self.base_url = base_url self.api_key = api_key register_provider("my-custom", MyCustomProvider) provider = get_provider("my-custom", base_url="http://localhost:8765") - Distribute as a pip plugin
toml # pyproject.toml [project.entry-points."praisonai.endpoint_providers"] my-custom = "mypkg.endpoints:MyCustomProvider"bash pip install my-praisonai-endpoint
How It Works
The registry is thread-safe and loads plugins lazily. Built-ins are discovered on first access, entry points are loaded when needed, and aliases are case-insensitive. The singleton registry is obtained via get_default_registry().
One Group, Two Readers
There is one canonical entry-point group: praisonai.endpoint_providers. Two readers scan it — the top-level provider registry (instantiates BaseProvider classes for serve / discovery) and the CLI dispatch registry (resolves endpoints invoke --type). One plugin contract serves both surfaces; the CLI adapts your BaseProvider subclass into its internal dispatch shape.
| Reader | Surface it powers |
|---|---|
praisonai.endpoints.registry.get_provider() | praisonai serve, discovery |
| CLI dispatch registry | praisonai endpoints invoke --type, praisonai endpoints types |
Built-in Dispatch Keys
| Key | Provider | Notes |
|---|---|---|
recipe | Recipe runner | Default when --type is omitted |
agents-api | Agents API | OpenAI Agents-API compatible |
mcp | MCP server | MCP server endpoint |
tools-mcp | MCP tools-only | Tools exposed as MCP |
a2a | A2A protocol | Agent-to-agent |
a2u | A2U protocol | Agent-to-user |
openai-compat | OpenAI-compatible HTTP | /v1/chat/completions, /v1/completions, /v1/models, /v1/tools/invoke |
Which Group Do I Publish Under?
Migration From the Deprecated Group
Rename the entry-point group in your pyproject.toml. The class and its methods are unchanged.
[project.entry-points."praisonai.endpoint_providers"]
my-custom = "mypkg.endpoints:MyCustomProvider"[project.entry-points."praisonai.endpoints.providers"]
my-custom = "mypkg.endpoints:MyCustomProvider"Deprecated-group or not, built-in names stay protected — a plugin named mcp under the legacy group cannot replace the built-in mcp provider (since PR #4185).
Constants exported by praisonai/endpoints/registry.py:
| Name | Value |
|---|---|
ENTRY_POINT_GROUP | "praisonai.endpoint_providers" |
LEGACY_ENTRY_POINT_GROUPS | ("praisonai.endpoints.providers",) |
Streaming
Implement invoke_stream — not invoke(stream=True) — to support --stream. When --stream is passed the CLI adapter calls provider.invoke_stream(name, input_data, config), which yields {"event": ..., "data": ...} frames. An event == "error" frame prints the error and returns exit code 3 (runtime error); other frames print as [event] data and it returns 0.
from typing import Any, Dict, Iterator, Optional
from praisonai.endpoints.providers.base import BaseProvider, InvokeResult
class MyStreamingProvider(BaseProvider):
def invoke(self, name, input_data=None, config=None, stream=False) -> InvokeResult:
return InvokeResult(ok=True, status="success", data={"echo": input_data})
def invoke_stream(
self,
name: str,
input_data: Optional[Dict[str, Any]] = None,
config: Optional[Dict[str, Any]] = None,
) -> Iterator[Dict[str, Any]]:
yield {"event": "start", "data": {"name": name}}
yield {"event": "token", "data": "hello"}
yield {"event": "done", "data": "[DONE]"}praisonai endpoints invoke chat_completions --type openai-compat \
--input-json '{"messages": [{"role": "user", "content": "Hi"}]}' \
--streamConfiguration / API
Functions
| Function / Method | Signature | Description |
|---|---|---|
register_provider(type, cls) | (str, Type[BaseProvider]) -> None | Backwards-compat module function; delegates to get_default_registry().register(...) |
get_provider(type, base_url, api_key, **kw) | (str, str, Optional[str], **Any) -> Optional[BaseProvider] | Resolve + instantiate; returns None for unknown type; raises if loader fails |
list_provider_types() | () -> List[str] | List registered + entry-point + built-in types |
get_provider_class(type) | (str) -> Optional[Type[BaseProvider]] | Returns the class without instantiating |
get_default_registry() | () -> ProviderRegistry | Thread-safe singleton accessor |
ProviderRegistry Class
| Method | Signature | Description |
|---|---|---|
ProviderRegistry.get(type, base_url, api_key, **kw) | identical to get_provider | OOP-style interface |
ProviderRegistry.list_types() | () -> List[str] | Back-compat alias for list_names() |
ProviderRegistry.get_class(type) | (str) -> Optional[Type] | Back-compat alias for resolve() |
Built-in Provider Types
| Type | Loaded From | Purpose |
|---|---|---|
recipe | endpoints/providers/recipe.py | Recipe runner endpoint |
agents-api | endpoints/providers/agents_api.py | OpenAI Agents-API compatible endpoint |
mcp | endpoints/providers/mcp.py | MCP server endpoint |
tools-mcp | endpoints/providers/tools_mcp.py | MCP tools-only endpoint |
a2a | endpoints/providers/a2a.py | A2A protocol endpoint |
a2u | endpoints/providers/a2u.py | A2U protocol endpoint |
openai-compat | endpoints/providers/openai_compat.py | OpenAI-compatible HTTP endpoint |
Common Patterns
Override a Built-in (runtime only)
The runtime register_provider() path is last-write-wins — it can replace a built-in in the current process:
from praisonai.endpoints.registry import register_provider
from praisonai.endpoints.providers.base import BaseProvider
class MyRecipeProvider(BaseProvider):
def __init__(self, base_url, api_key=None, **kwargs):
# Custom implementation
pass
# Override built-in recipe provider at runtime
register_provider("recipe", MyRecipeProvider)Multiple Aliases
Use the underlying PluginRegistry for alias support:
from praisonai.endpoints.registry import get_default_registry
registry = get_default_registry()
registry.register_lazy("my-provider", lambda: MyProvider, aliases=["mp", "custom"])Per-tenant Isolation
Instantiate ProviderRegistry directly for isolation:
from praisonai.endpoints.registry import ProviderRegistry
# Tenant-specific registry
tenant_registry = ProviderRegistry()
tenant_registry.register("custom", TenantProvider)Best Practices
Always provide a _loader() function
Never import your provider class at module top-level — defeats lazy loading and breaks the no-heavy-deps-at-import-time guarantee:
# Good - lazy loader
def _load_my_provider():
from .heavy_dependency import MyProvider
return MyProvider
# Bad - top-level import
from .heavy_dependency import MyProvider # Loaded immediately!Subclass BaseProvider
Required for type compatibility and consistent API:
from praisonai.endpoints.providers.base import BaseProvider
class MyProvider(BaseProvider):
def __init__(self, base_url, api_key=None, **kwargs):
super().__init__(base_url, api_key, **kwargs)Distinguish missing vs import failure
get_provider("unknown") returns None, but import failures propagate:
provider = get_provider("unknown")
if provider is None:
print("Provider type not registered")
try:
provider = get_provider("registered-but-missing-deps")
except ValueError as e:
print(f"Provider exists but dependencies missing: {e}")Prefer entry points for distributable packages
Use pyproject.toml entry points instead of register_provider() calls:
# Preferred - discoverable via pip install
[project.entry-points."praisonai.endpoint_providers"]
my-provider = "my_package.providers:MyProvider"
# Avoid - requires explicit registration
# register_provider("my-provider", MyProvider)