How to Add New LLM Providers Beyond Ollama in MoneyPrinterV2

You can add new LLM providers to MoneyPrinterV2 by creating an abstract LLMProvider interface, implementing provider-specific classes (like OpenAIProvider), and adding a factory function that instantiates the correct backend based on a config.json flag, while keeping the public generate_text() API unchanged.

MoneyPrinterV2 currently ships with a thin wrapper around the local Ollama server located in src/llm_provider.py. To integrate cloud providers like OpenAI, Anthropic, or Google Gemini without refactoring the entire codebase, you need to extend this abstraction layer. The following guide walks through the exact architecture and implementation steps required to make MoneyPrinterV2 provider-agnostic.

Current Architecture in MoneyPrinterV2

The existing code in src/llm_provider.py exposes three core functions that the rest of the application consumes:

Function Purpose Implementation Details
list_models() Returns available model names Calls ollama.Client().list() and extracts model names from the response【/cache/repos/github.com/FujiwaraChoki/MoneyPrinterV2/main/src/llm_provider.py#L12-L20】
select_model(model: str) Sets the active model for subsequent calls Stores the name in a module-level variable _selected_model【/cache/repos/github.com/FujiwaraChoki/MoneyPrinterV2/main/src/llm_provider.py#L23-L31】
generate_text(prompt: str, model_name: str = None) -> str Sends a prompt and returns generated text Uses ollama.Client().chat() with the selected model【/cache/repos/github.com/FujiwaraChoki/MoneyPrinterV2/main/src/llm_provider.py#L41-L63】

All higher-level components like YouTube and Twitter classes import generate_text directly:

from llm_provider import generate_text

Step-by-Step Implementation Guide

Step 1: Define the Provider Interface

Create an abstract base class at the top of src/llm_provider.py that defines the contract all providers must implement:


# src/llm_provider.py (add at top)

from abc import ABC, abstractmethod

class LLMProvider(ABC):
    @abstractmethod
    def list_models(self) -> list[str]:
        ...
    
    @abstractmethod
    def generate_text(self, prompt: str, model_name: str | None = None) -> str:
        ...
    
    def select_model(self, model: str) -> None:
        """Optional default implementation"""
        self._selected_model = model

Step 2: Refactor Ollama into a Concrete Provider

Move the existing Ollama-specific logic into a class that implements LLMProvider:


# src/llm_provider.py (after the interface)

class OllamaProvider(LLMProvider):
    def __init__(self):
        self._client = ollama.Client(host=get_ollama_base_url())
        self._selected_model: str | None = None

    def list_models(self) -> list[str]:
        response = self._client.list()
        return sorted(m.model for m in response.models)

    def select_model(self, model: str) -> None:
        self._selected_model = model

    def generate_text(self, prompt: str, model_name: str | None = None) -> str:
        model = model_name or self._selected_model
        if not model:
            raise RuntimeError("No model selected")
        resp = self._client.chat(
            model=model, 
            messages=[{"role": "user", "content": prompt}]
        )
        return resp["message"]["content"].strip()

Step 3: Implement a New Provider (OpenAI Example)

Add a new provider class for OpenAI. Install the SDK first (pip install openai), then implement:


# src/llm_provider.py (new class)

import os
import openai

class OpenAIProvider(LLMProvider):
    def __init__(self):
        openai.api_key = os.getenv("OPENAI_API_KEY")
        self._selected_model = "gpt-4o-mini"  # default fallback

    def list_models(self) -> list[str]:
        models = openai.Model.list()
        return [m.id for m in models.data if "gpt" in m.id]

    def select_model(self, model: str) -> None:
        self._selected_model = model

    def generate_text(self, prompt: str, model_name: str | None = None) -> str:
        model = model_name or self._selected_model
        response = openai.ChatCompletion.create(
            model=model,
            messages=[{"role": "user", "content": prompt}],
            temperature=0.7,
        )
        return response.choices[0].message.content.strip()

Security Note: The class fetches the API key from the environment variable OPENAI_API_KEY, maintaining the same security pattern used elsewhere in MoneyPrinterV2.

Step 4: Create the Provider Factory

Add a factory function that instantiates the correct provider based on config.json:


# src/llm_provider.py (bottom of file)

_provider: LLMProvider | None = None

def _load_provider() -> LLMProvider:
    """Instantiate the provider defined in config.json."""
    import json
    import os
    
    cfg_path = os.path.join(
        os.path.dirname(os.path.abspath(__file__)), 
        "..", 
        "config.json"
    )
    
    with open(cfg_path) as f:
        cfg = json.load(f)
    
    name = cfg.get("llm_provider", "ollama").lower()

    if name == "ollama":
        return OllamaProvider()
    elif name in ("openai", "openai_chat"):
        return OpenAIProvider()
    else:
        raise ValueError(f"Unsupported LLM provider: {name}")

def _ensure_provider() -> LLMProvider:
    global _provider
    if _provider is None:
        _provider = _load_provider()
    return _provider

Step 5: Update Public API Functions

Rewrite the original module-level functions to delegate to the active provider:


# src/llm_provider.py (replace old functions)

def list_models() -> list[str]:
    return _ensure_provider().list_models()

def select_model(model: str) -> None:
    _ensure_provider().select_model(model)

def get_active_model() -> str | None:
    prov = _ensure_provider()
    return getattr(prov, "_selected_model", None)

def generate_text(prompt: str, model_name: str = None) -> str:
    return _ensure_provider().generate_text(prompt, model_name)

Step 6: Update Configuration and Dependencies

Add the new provider flag to config.json:

{
    "llm_provider": "openai",
    "ollama_base_url": "http://127.0.0.1:11434",
    "ollama_model": "llama3.2:3b",
    "openai_api_key": "${OPENAI_API_KEY}"
}

Update requirements.txt to include the new SDK:

openai>=1.30.0  # for OpenAI LLM support

Key Files in MoneyPrinterV2

File Role Direct Link
src/llm_provider.py Core abstraction for LLM interactions (currently Ollama only) src/llm_provider.py
src/config.py Helper to read config.json values such as ollama_base_url and the new llm_provider flag src/config.py
src/classes/YouTube.py Calls generate_text for topic/script/metadata generation src/classes/YouTube.py
src/main.py CLI entry point; selects model on start-up using list_models and select_model src/main.py
requirements.txt Declares third-party packages; add new provider dependencies here requirements.txt

Summary

  • Abstract the provider logic by creating an LLMProvider base class that defines list_models(), select_model(), and generate_text().
  • Refactor the existing Ollama code into OllamaProvider to maintain backward compatibility while formalizing the interface.
  • Implement new providers (e.g., OpenAIProvider) by inheriting from LLMProvider and translating the standardized methods to provider-specific SDK calls.
  • Use a factory pattern in src/llm_provider.py to instantiate the correct provider based on a llm_provider key in config.json.
  • Preserve the public API so that YouTube.py, Twitter.py, and main.py continue importing generate_text without modification.

Frequently Asked Questions

What is the default LLM provider in MoneyPrinterV2?

The default provider is Ollama, configured via the ollama_base_url and ollama_model keys in config.json. The system initializes an OllamaProvider instance when the llm_provider configuration key is missing or set to "ollama".

Do I need to modify the YouTube or Twitter classes when adding a new provider?

No. The YouTube and Twitter classes in src/classes/ import generate_text from llm_provider.py as a module-level function. Because you will maintain the same function signatures (list_models, select_model, generate_text) and delegate to the active provider internally, these classes require zero changes.

How do I securely store API keys for cloud providers like OpenAI?

Follow the existing pattern in MoneyPrinterV2: store the key in an environment variable (e.g., OPENAI_API_KEY) and read it via os.getenv() inside your provider's __init__ method. Never hardcode secrets in config.json or source files. If you add the key to config.json, use a placeholder like "${OPENAI_API_KEY}" and resolve it at runtime.

Can I switch between providers without restarting the application?

By default, the factory caches the provider instance in a module-level _provider variable. To enable runtime switching, you would need to expose a setter function (e.g., set_provider(name: str)) that resets _provider to None or assigns a new instance, then call _load_provider() again. This is optional and not required for basic multi-provider support.

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 →