How to Add a New AI Provider to the Esperanto-Based System in Open Notebook

Adding a new AI provider requires registering environment variables in key_provider.py, implementing a discovery coroutine in model_discovery.py, and optionally extending the Credential class for complex authentication scenarios.

Open Notebook uses the Esperanto library as its unified front-end for all AI providers. When you add a new AI provider to this Esperanto-based system, you integrate three core touchpoints: credential provisioning, model discovery, and type classification. These modifications enable the system to automatically discover models, classify them by capability (language, embedding, STT, TTS), and inject secure credentials at runtime.

Step 1: Register the Provider's Environment Variables

Start by mapping your provider name to the environment variables that Esperanto expects. In open_notebook/ai/key_provider.py, add an entry to the PROVIDER_CONFIG dictionary:


# open_notebook/ai/key_provider.py

PROVIDER_CONFIG = {
    # Existing entries …

    "myprovider": {
        "env_var": "MYPROVIDER_API_KEY",
        # For URL-based providers, add a second entry:

        # "base_url_var": "MYPROVIDER_API_BASE",

    },
}

The provision_provider_keys() function looks up PROVIDER_CONFIG to copy database-stored credentials into the process environment. This ensures that when a model of provider myprovider is requested, Esperanto can fetch its configuration from the environment or from a Credential record.

Step 2: Implement Model Discovery Logic

If your provider publishes an API that lists available models, implement an async discovery coroutine in open_notebook/ai/model_discovery.py. This function fetches model metadata and returns standardized DiscoveredModel objects:


# open_notebook/ai/model_discovery.py

async def discover_myprovider_models() -> List[DiscoveredModel]:
    """Fetch the list of models from MyProvider's /v1/models endpoint."""
    api_key = os.getenv("MYPROVIDER_API_KEY")
    base_url = os.getenv("MYPROVIDER_API_BASE", "https://api.myprovider.com")
    
    if not api_key:
        return []
    
    models: List[DiscoveredModel] = []
    try:
        async with httpx.AsyncClient() as client:
            resp = await client.get(
                f"{base_url}/v1/models",
                headers={"Authorization": f"Bearer {api_key}"},
                timeout=30.0,
            )
            resp.raise_for_status()
            for m in resp.json().get("models", []):
                name = m.get("id")
                if name:
                    model_type = classify_model_type(name, "myprovider")
                    models.append(
                        DiscoveredModel(
                            name=name,
                            provider="myprovider",
                            model_type=model_type,
                        )
                    )
    except Exception as exc:
        logger.warning(f"Failed to discover MyProvider models: {exc}")
    return models

After defining the coroutine, register it in the PROVIDER_DISCOVERY_FUNCTIONS dictionary at the bottom of the same file:


# open_notebook/ai/model_discovery.py

PROVIDER_DISCOVERY_FUNCTIONS["myprovider"] = discover_myprovider_models

The sync_all_providers() function iterates over PROVIDER_DISCOVERY_FUNCTIONS to automatically populate the model table, making new models selectable in the UI without manual database inserts.

Step 3: Update Provider Type Classification

Teach the classify_model_type helper how to recognize model capabilities for your new provider. If your provider uses predictable naming conventions (e.g., "embed-" for embeddings), extend the type mappings:


# open_notebook/ai/model_discovery.py

MYPROVIDER_MODEL_TYPES = {
    "language": ["gpt", "chat", "llm"],
    "embedding": ["embed"],
    "speech_to_text": ["stt"],
    "text_to_speech": ["tts"],
}

# Add to the type_mappings dict inside classify_model_type

type_mappings["myprovider"] = MYPROVIDER_MODEL_TYPES

Accurate classification guarantees that ModelManager.get_default_model() returns the correct model subtype—LanguageModel, EmbeddingModel, SpeechToTextModel, or TextToSpeechModel—which determines what methods are available on the instance.

Step 4: Handle Complex Authentication (Optional)

If your provider requires more than a simple API key (e.g., Azure endpoints, Vertex service accounts, or project IDs), extend the Credential domain model in open_notebook/domain/credential.py:


# open_notebook/domain/credential.py

class Credential(ObjectModel):
    # Existing fields…

    myprovider_project: Optional[str] = None
    myprovider_region: Optional[str] = None
    
    def to_esperanto_config(self) -> Dict[str, Any]:
        cfg = super().to_esperanto_config()
        if self.myprovider_project:
            cfg["project"] = self.myprovider_project
        if self.myprovider_region:
            cfg["region"] = self.myprovider_region
        return cfg

The to_esperanto_config() method builds the configuration dictionary that Esperanto receives when instantiating a model. This keeps sensitive credentials out of the codebase while ensuring they are available at runtime.

End-to-End Integration Flow

Once you have added a new AI provider to the Esperanto-based system, the integration follows this execution path:

  1. Credential Provisioning: When a workflow requests a model, ModelManager.get_model() loads the Model record and any linked Credential, then calls provision_provider_keys() to inject environment variables.
  2. Model Instantiation: The manager calls AIFactory.create_*() with the provider name and the config dict from to_esperanto_config().
  3. Discovery: Running await sync_provider_models("myprovider") (or clicking "Sync Models" in the UI) executes your discovery coroutine, inserts Model rows into SurrealDB via Model.save(), and classifies each model by type.
  4. UI Exposure: The /api/routers/models endpoint serves these records to the frontend, populating Settings → Models automatically.

Using the New Model in Workflows

from open_notebook.ai.models import model_manager

# Retrieve the default language model for MyProvider

my_model = await model_manager.get_model(
    model_id="open_notebook:myprovider:gpt-4",
    temperature=0.7,
)

# Generate text

response = await my_model.generate("Explain quantum computing")

Summary

  • Register environment variables in open_notebook/ai/key_provider.py by adding to PROVIDER_CONFIG so provision_provider_keys() can inject credentials.
  • Implement discovery logic in open_notebook/ai/model_discovery.py by creating an async function and registering it in PROVIDER_DISCOVERY_FUNCTIONS.
  • Update type classification in classify_model_type() to map model names to language, embedding, STT, or TTS capabilities.
  • Extend credentials in open_notebook/domain/credential.py for non-standard auth flows, overriding to_esperanto_config() to expose custom fields.
  • No UI changes required—the discovery step automatically registers Model records that appear in the Settings interface.

Frequently Asked Questions

Do I need to modify the frontend code to add a new provider?

No. The UI pulls data from /api/routers/models, which uses ModelManager to query the database. Once your discovery function populates the model table, the provider automatically appears in the Models → Add screen. You only need to modify UI code if you want custom credential input fields beyond the standard API key.

How does the model type classification determine which Esperanto class to instantiate?

The classify_model_type() function returns a string—"language", "embedding", "speech_to_text", or "text_to_speech"—which ModelManager uses to call the appropriate AIFactory.create_language_model(), create_embedding_model(), create_speech_to_text_model(), or create_text_to_speech_model() method. Incorrect classification results in runtime errors when calling provider-specific methods.

What if my AI provider does not expose a model listing endpoint?

If the provider lacks a discovery API, modify your discovery function to return a static list of DiscoveredModel objects instead of making an HTTP request. Hardcode the known model IDs and types, then register the function normally. The system will treat these as discoverable models, and you can still update the list by deploying code changes.

How are credentials securely injected when using the new provider?

The ModelManager loads the Credential record linked to the model, calls to_esperanto_config() to build a configuration dictionary, and temporarily sets the environment variables defined in PROVIDER_CONFIG before instantiating the Esperanto client. This isolation ensures that API keys never leave the server process and are never exposed to the frontend.

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 →