# How to Add New LLM Providers Beyond Ollama in MoneyPrinterV2

> Extend MoneyPrinterV2 beyond Ollama by implementing new LLM providers. Learn to create provider interfaces and factory functions for seamless integration and maintain the generate text API.

- Repository: [FujiwaraChoki/MoneyPrinterV2](https://github.com/FujiwaraChoki/MoneyPrinterV2)
- Tags: how-to-guide
- Published: 2026-03-20

---

**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`](https://github.com/FujiwaraChoki/MoneyPrinterV2/blob/main/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`](https://github.com/FujiwaraChoki/MoneyPrinterV2/blob/main/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`](https://github.com/FujiwaraChoki/MoneyPrinterV2/blob/main/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:

```python
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`](https://github.com/FujiwaraChoki/MoneyPrinterV2/blob/main/src/llm_provider.py) that defines the contract all providers must implement:

```python

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

```python

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

```python

# 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`](https://github.com/FujiwaraChoki/MoneyPrinterV2/blob/main/config.json):

```python

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

```python

# 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`](https://github.com/FujiwaraChoki/MoneyPrinterV2/blob/main/config.json):

```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`](https://github.com/FujiwaraChoki/MoneyPrinterV2/blob/main/requirements.txt) to include the new SDK:

```text
openai>=1.30.0  # for OpenAI LLM support

```

## Key Files in MoneyPrinterV2

| File | Role | Direct Link |
|------|------|-------------|
| [`src/llm_provider.py`](https://github.com/FujiwaraChoki/MoneyPrinterV2/blob/main/src/llm_provider.py) | Core abstraction for LLM interactions (currently Ollama only) | [src/llm_provider.py](https://github.com/FujiwaraChoki/MoneyPrinterV2/blob/main/src/llm_provider.py) |
| [`src/config.py`](https://github.com/FujiwaraChoki/MoneyPrinterV2/blob/main/src/config.py) | Helper to read [`config.json`](https://github.com/FujiwaraChoki/MoneyPrinterV2/blob/main/config.json) values such as `ollama_base_url` and the new `llm_provider` flag | [src/config.py](https://github.com/FujiwaraChoki/MoneyPrinterV2/blob/main/src/config.py) |
| [`src/classes/YouTube.py`](https://github.com/FujiwaraChoki/MoneyPrinterV2/blob/main/src/classes/YouTube.py) | Calls `generate_text` for topic/script/metadata generation | [src/classes/YouTube.py](https://github.com/FujiwaraChoki/MoneyPrinterV2/blob/main/src/classes/YouTube.py) |
| [`src/main.py`](https://github.com/FujiwaraChoki/MoneyPrinterV2/blob/main/src/main.py) | CLI entry point; selects model on start-up using `list_models` and `select_model` | [src/main.py](https://github.com/FujiwaraChoki/MoneyPrinterV2/blob/main/src/main.py) |
| [`requirements.txt`](https://github.com/FujiwaraChoki/MoneyPrinterV2/blob/main/requirements.txt) | Declares third-party packages; add new provider dependencies here | [requirements.txt](https://github.com/FujiwaraChoki/MoneyPrinterV2/blob/main/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`](https://github.com/FujiwaraChoki/MoneyPrinterV2/blob/main/src/llm_provider.py) to instantiate the correct provider based on a `llm_provider` key in [`config.json`](https://github.com/FujiwaraChoki/MoneyPrinterV2/blob/main/config.json).
- **Preserve the public API** so that [`YouTube.py`](https://github.com/FujiwaraChoki/MoneyPrinterV2/blob/main/YouTube.py), [`Twitter.py`](https://github.com/FujiwaraChoki/MoneyPrinterV2/blob/main/Twitter.py), and [`main.py`](https://github.com/FujiwaraChoki/MoneyPrinterV2/blob/main/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`](https://github.com/FujiwaraChoki/MoneyPrinterV2/blob/main/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`](https://github.com/FujiwaraChoki/MoneyPrinterV2/blob/main/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`](https://github.com/FujiwaraChoki/MoneyPrinterV2/blob/main/config.json) or source files. If you add the key to [`config.json`](https://github.com/FujiwaraChoki/MoneyPrinterV2/blob/main/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.