Endpoint Provider Registry

Plug custom endpoint providers (A2A, MCP, recipe, agents-api…) into PraisonAI via Python entry points

python
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.

bash
export PRAISON_ENDPOINT_URL="http://localhost:8765"
praisonai serve --endpoint mcp
python
from 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.

Diagram

📦 Your pip package

🔌 praisonai.endpoint_providers

📝 ProviderRegistry
get_provider

⌨️ CLI dispatch
endpoints invoke –type

✅ serve / discovery

Quick Start

  1. Use a built-in provider
    python
    from praisonai.endpoints.registry import get_provider
    
    provider = get_provider("recipe", base_url="http://localhost:8765")
  2. 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")
  3. 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

Diagram
MyCustomProviderimportlib.metadata entry_pointsProviderRegistryget_provider(“my-custom”, …)UserMyCustomProviderimportlib.metadata entry_pointsProviderRegistryget_provider(“my-custom”, …)Userget_provider(“my-custom”, base_url, api_key, **kw)resolve(“my-custom”)load praisonai.endpoint_providersMyCustomProvider classclass__init__(base_url=…, api_key=…, **kw)provider instance

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.

ReaderSurface it powers
praisonai.endpoints.registry.get_provider()praisonai serve, discovery
CLI dispatch registrypraisonai endpoints invoke --type, praisonai endpoints types

Built-in Dispatch Keys

KeyProviderNotes
recipeRecipe runnerDefault when --type is omitted
agents-apiAgents APIOpenAI Agents-API compatible
mcpMCP serverMCP server endpoint
tools-mcpMCP tools-onlyTools exposed as MCP
a2aA2A protocolAgent-to-agent
a2uA2U protocolAgent-to-user
openai-compatOpenAI-compatible HTTP/v1/chat/completions, /v1/completions, /v1/models, /v1/tools/invoke
Diagram
EndpointBaseProvider subclassCLI dispatch registryendpoints invoke –typeEndpointBaseProvider subclassCLI dispatch registryendpoints invoke –typeresolve(type=”mcp”)provider classinvoke(name, input, config)handle requestresponse

Which Group Do I Publish Under?

Diagram

New plugin

Existing plugin on the old group

Publishing an
endpoint provider?

✅ praisonai.endpoint_providers

⚠️ Still works,
warns on import


Migration From the Deprecated Group

Rename the entry-point group in your pyproject.toml. The class and its methods are unchanged.

pyproject.toml (canonical)
[project.entry-points."praisonai.endpoint_providers"]
my-custom = "mypkg.endpoints:MyCustomProvider"
pyproject.toml (deprecated — warns)
[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:

NameValue
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.

python
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]"}
bash
praisonai endpoints invoke chat_completions --type openai-compat \
  --input-json '{"messages": [{"role": "user", "content": "Hi"}]}' \
  --stream

Configuration / API

Functions

Function / MethodSignatureDescription
register_provider(type, cls)(str, Type[BaseProvider]) -> NoneBackwards-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()() -> ProviderRegistryThread-safe singleton accessor

ProviderRegistry Class

MethodSignatureDescription
ProviderRegistry.get(type, base_url, api_key, **kw)identical to get_providerOOP-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

TypeLoaded FromPurpose
recipeendpoints/providers/recipe.pyRecipe runner endpoint
agents-apiendpoints/providers/agents_api.pyOpenAI Agents-API compatible endpoint
mcpendpoints/providers/mcp.pyMCP server endpoint
tools-mcpendpoints/providers/tools_mcp.pyMCP tools-only endpoint
a2aendpoints/providers/a2a.pyA2A protocol endpoint
a2uendpoints/providers/a2u.pyA2U protocol endpoint
openai-compatendpoints/providers/openai_compat.pyOpenAI-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:

python
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:

python
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:

python
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:

python
# 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:

python
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:

python
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:

toml
# 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)

Related