Plugin Precedence

Built-in names always win over entry-point plugins — one rule, enforced in the base PluginRegistry for every registry

Every PraisonAI registry follows one rule: a built-in name always wins over an entry-point plugin — whichever entry-point group the plugin publishes under, canonical or a deprecated legacy spelling. Publishing a pip package with an entry point named openai, docker, or aws has no effect — the built-in resolves. Runtime register(...) is the only deliberate override path.

python
from praisonai.llm.registry import get_default_llm_registry

# A pip plugin named "openai" is silently dropped (DEBUG log).
# The supported override is an explicit, in-process register(...) call:
def _my_openai():
    from my_pkg import MyOpenAI
    return MyOpenAI

get_default_llm_registry().register("openai", _my_openai, override=True)

The plugin author does not accidentally hijack a built-in through a pyproject.toml; overriding a built-in is always a deliberate, in-process decision.

Diagram

Plugin Precedence

yes

no

🧩 Entry-point discovery

🔍 Collides with
a built-in name?

⏭️ Skip plugin
keep built-in
(DEBUG log)

➕ Add plugin

🛠️ Runtime register(…)

✅ Overrides built-in
(deliberate DI)

Quick Start

  1. Pick a name that does not collide

    Names are matched case-insensitively against the built-ins of the registry you target. openai, OpenAI, and OPENAI are the same key — pick something novel.

    toml
    # pyproject.toml — additive plugin, resolves fine
    [project.entry-points."praisonai.llm_providers"]
    acme-llm = "acme_llm:AcmeProvider"
  2. Override a built-in deliberately (runtime only)

    The one supported way to replace a built-in is an in-process register(...) call:

    python
    from praisonai.llm.registry import get_default_llm_registry
    
    def _my_openai():
        from acme_llm import PatchedOpenAI
        return PatchedOpenAI
    
    get_default_llm_registry().register("openai", _my_openai, override=True)
  3. Confirm what resolved

    Turn on DEBUG logging to see the collision line noting that a shipped built-in kept its name.

    bash
    LOGLEVEL=DEBUG praisonai run "hello"

How It Works

Since PraisonAI PR #4176 the guard lives in the base PluginRegistry, so every registry that subclasses it inherits the behaviour. It is enforced once, not re-asserted per registry.

Some registries also honour deprecated (legacy) entry-point group spellings for backward compatibility. Legacy discovery runs first (so the canonical spelling wins on a clash), and since PraisonAI PR #4185 it applies the same built-in check — you cannot resurrect the old shadowing behaviour by falling back to a deprecated group.

Standalone installs of praisonai-sandbox and praisonai-deploy carry a vendored fallback copy of PluginRegistry that runs when praisonai-code isn’t importable (pip install praisonai-sandbox on its own). Since PraisonAI PR #4184 both vendored copies enforce the same built-in check — an entry point whose name (case-insensitive) matches a built-in is skipped on that path too, so the guarantee holds in every install shape. Prior to PR #4184 a standalone praisonai-sandbox install still let a pip plugin replace docker, and praisonai-deploy let one replace aws.

Diagram
Caller_builtin_namesPluginRegistrypip packageCaller_builtin_namesPluginRegistrypip packagealt[collision]alt[collision][novel name]legacy-group entry point “mcp” (DeprecationWarning)lowercase(“mcp”) in built-ins?yesskip entry point (DEBUG log), keep built-incanonical entry point “openai”lowercase(“openai”) in built-ins?yesskip entry point (DEBUG log), keep built-innoadd pluginregister(“openai”, loader, override=True)override wins (deliberate DI)

The same rule runs on the vendored fallback path used by standalone praisonai-sandbox / praisonai-deploy installs:

Diagram
_builtin_namesvendored PluginRegistry(praisonai-sandbox / praisonai-deploystandalone)pip package_builtin_namesvendored PluginRegistry(praisonai-sandbox / praisonai-deploystandalone)pip packagealt[collision][novel name]entry point “docker” (sandbox) / “aws” (deploy)lowercase(name) in built-ins?yesskip entry point (DEBUG log), keep built-innoadd plugin
PathOutcome
Entry point under the canonical group, name collides with a built-inSkipped; built-in kept. A DEBUG (not WARNING) line notes the collision.
Entry point under a deprecated/legacy group, name collides with a built-inSkipped; built-in kept (DEBUG log) — same rule, applied on the legacy path since PR #4185.
Entry point discovered by a vendored fallback copy (standalone praisonai-sandbox / praisonai-deploy), name collides with a built-inSkipped; built-in kept (DEBUG log) — same rule, applied on the fallback path since PR #4184.
Entry point under a legacy group, novel nameAdded, and emits a DeprecationWarning naming the canonical group.
Entry point, novel nameAdded; resolves and appears in listings.
Runtime register(...)Overrides a built-in — deliberate dependency injection.
MatchingCase-insensitive — OpenAI and openai are the same key.
Plugin-provided tool (Plugin.get_tools()) collides with an agent tool (Agent(tools=[...]))Agent tool wins; the plugin’s same-named tool is skipped with a WARNING log — never silently shadowed. See Tool Plugins.

Every Registry Is Guarded

The two groups that matter most decide where user code executes: praisonai.sandbox and praisonai.managed_backends. A pip-installed package cannot silently take over docker — whether via the canonical base class (PR #4176), the legacy group spelling (PR #4185), or the vendored fallback copy that runs when praisonai-sandbox / praisonai-deploy is installed standalone (PR #4184).

Entry-point groupRegistry classRuntime accessorExample guarded built-ins
praisonai.llm_providersLLMProviderRegistryget_default_llm_registry()openai, anthropic, google, ollama, ollama_chat, lm_studio, vllm, hosted_vllm
praisonai.sandboxsandbox registry(sandbox registry)docker, local, e2b
praisonai.managed_backendsManagedBackendRegistryget_backend_registry()docker
praisonai.deploy.providersdeploy registry(deploy registry)aws, azure, gcp, fly, railway, render
praisonai.tools.searchSearchProviderRegistry(search registry)tavily
praisonai.endpoints.providersEndpointProviderRegistryget_default_registry()mcp
praisonai.external_agentsExternalAgentRegistryget_default_registry()claude, gemini, codex, cursor
praisonai.tool_sources / praisonai.toolsToolSourceRegistry(tool resolver)all built-in tool sources
praisonai.integrationsIntegrationRegistryget_integrations_registry()all built-in integration names
praisonai.persistenceStoreRegistryget_default_registry(kind)all built-in stores
(framework adapters)FrameworkAdapterRegistryget_default_registry()all built-in adapters

Compatibility


Best Practices

Namespace your plugin names

Prefix or vendor-qualify a plugin name (acme-openai, corp.pay_invoice) so it never collides with a built-in on any surface, present or future.

Reserve register(…) for deliberate overrides

A runtime register(...) call is in-process and explicit — exactly what you want when overriding a built-in is intentional (a tenant-specific provider, a test double). It cannot happen by accident through packaging.

Check collisions case-insensitively

OpenAI, openai, and OPENAI are one key. When you audit a registry’s built-ins before naming a plugin, lowercase both sides.

Debug with DEBUG logging

A skipped collision writes a DEBUG line, not a warning. Run with LOGLEVEL=DEBUG to confirm which built-in kept its name.


Related