How to Add New Providers to OpenMontage Without Code Changes Using the Selector-Provider Pattern

You can add new providers to OpenMontage by creating a single Python file that subclasses BaseTool and declares the target capability; the ToolRegistry automatically discovers it at runtime, enabling selectors to route requests to your new provider without modifying any existing code.

OpenMontage separates request routing from service implementation through the selector-provider pattern, a capability-based architecture that eliminates hard-coded provider dependencies. This design allows developers to integrate new AI video, image, or audio generation services simply by dropping a new module into the tools/ directory tree, without touching selectors or registry configuration.

Understanding the Selector-Provider Architecture

OpenMontage organizes tools into two distinct roles that communicate through capabilities.

Selectors act as routing façades. For example, VideoSelector in tools/video/video_selector.py declares provider = "selector" and the capability "video_generation". It never hard-codes provider names. Instead, its _providers() method queries the registry at runtime:

def _providers(self) -> list[BaseTool]:
    from tools.tool_registry import registry
    registry.ensure_discovered()                     # discovers all tool modules

    return [t for t in registry.get_by_capability("video_generation")
            if t.name != self.name]                  # excludes self, returns providers

Providers are concrete implementations of BaseTool that declare the same capability (e.g., "video_generation") but unique provider identifiers (e.g., "fal", "heygen", "custom_ai"). When VideoSelector receives a request, it builds a candidate list from these discovered providers, scores them using lib.scoring.rank_providers, and executes the best match.

Step-by-Step Guide to Adding a New Provider

1. Create the Provider Module

Create a new Python file under the appropriate sub-package. For video generation, place it under tools/video/:

touch tools/video/my_new_provider.py

2. Subclass BaseTool and Declare Metadata

Inherit from BaseTool (defined in tools/base_tool.py) and set the required class attributes. The capability must match the selector you wish to integrate with:

from tools.base_tool import BaseTool, ToolResult, ToolTier, ToolRuntime, ToolStability

class MyNewProvider(BaseTool):
    # Identity

    name = "my_new_provider"
    provider = "mynew"
    capability = "video_generation"
    tier = ToolTier.GENERATE
    runtime = ToolRuntime.HYBRID
    stability = ToolStability.BETA
    
    # Optional metadata for scoring

    best_for = ["quick_prototypes", "low_cost"]
    
    # Input validation schema

    input_schema = {
        "type": "object",
        "required": ["prompt"],
        "properties": {"prompt": {"type": "string"}},
    }

3. Implement Required Methods

You must implement estimate_cost, estimate_runtime, and execute. These enable the selector to perform resource estimation and actual generation:

    def estimate_cost(self, inputs: dict) -> float:
        """Return estimated cost in USD."""
        return 0.02

    def estimate_runtime(self, inputs: dict) -> float:
        """Return estimated execution time in seconds."""
        return 5.0

    def execute(self, inputs: dict) -> ToolResult:
        """Call the provider API and return results."""
        # Integration with your AI service happens here

        video_url = self._call_external_api(inputs["prompt"])
        return ToolResult(success=True, data={"video_url": video_url})

4. Verify Registration

Once the file is saved, the registry will discover it automatically. You can verify registration at runtime:

from tools.tool_registry import registry

registry.ensure_discovered()
providers = [t.name for t in registry.get_by_capability("video_generation")]
print(providers)

# Output includes: ['fal', 'heygen', 'my_new_provider', ...]

How Runtime Discovery Works

The tool registry in tools/tool_registry.py implements auto-discovery using Python's pkgutil.walk_packages. When registry.ensure_discovered() runs, it imports every module under the tools/ directory tree once per process, building an in-memory catalog of all BaseTool subclasses.

Because selectors retrieve providers via registry.get_by_capability("video_generation") rather than static imports, the candidate list updates dynamically whenever new modules are added. The ranking and fallback mechanisms in lib.scoring.rank_providers operate on this dynamic list, ensuring the selector's routing logic remains unchanged regardless of how many providers exist.

This architecture means extending OpenMontage requires only adding a new file that fulfills the BaseTool contract. No configuration files, import statements, or selector modifications are necessary.

Summary

  • Capability-based discovery: Selectors query the registry by capability (e.g., "video_generation"), not by hard-coded provider names.
  • Zero-modification extension: Adding my_new_provider.py under tools/video/ automatically registers the provider without touching VideoSelector or ToolRegistry.
  • Auto-import mechanism: ToolRegistry.ensure_discovered() uses pkgutil.walk_packages to load all modules under tools/ at startup.
  • Consistent interface: All providers must subclass BaseTool and implement estimate_cost, estimate_runtime, and execute.
  • Dynamic routing: The selector's _providers() method builds candidate lists at runtime, enabling immediate participation of new providers in the ranking and execution flow.

Frequently Asked Questions

Do I need to register my new provider in a configuration file?

No. The ToolRegistry automatically discovers any Python module placed under the tools/ directory using pkgutil.walk_packages. As long as your class subclasses BaseTool and is syntactically valid, it will be imported and registered when registry.ensure_discovered() executes.

What capability value should I use for my provider?

You must declare a capability string that matches the selector's declared capability. For example, use "video_generation" to be discovered by VideoSelector in tools/video/video_selector.py, or "image_generation" for image selectors. The capability string acts as the routing contract between selectors and providers.

Can I override the selector's automatic provider choice?

Yes. Pass a preferred_provider parameter in your execution inputs (e.g., {"preferred_provider": "mynew"}). The selector's ranking engine in lib.scoring.rank_providers weighs this preference heavily when scoring candidates, though it may override the preference if the specified provider cannot fulfill the request's technical constraints or estimated cost limits.

What methods must I implement when subclassing BaseTool?

You must implement estimate_cost, estimate_runtime, and execute. Optionally, you can define input_schema as a JSON Schema dictionary to enable automatic input validation. These methods are defined in tools/base_tool.py and are required for the selector to estimate resource usage and execute generation tasks.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →