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
LLMProviderbase class that defineslist_models(),select_model(), andgenerate_text(). - Refactor the existing Ollama code into
OllamaProviderto maintain backward compatibility while formalizing the interface. - Implement new providers (e.g.,
OpenAIProvider) by inheriting fromLLMProviderand translating the standardized methods to provider-specific SDK calls. - Use a factory pattern in
src/llm_provider.pyto instantiate the correct provider based on allm_providerkey inconfig.json. - Preserve the public API so that
YouTube.py,Twitter.py, andmain.pycontinue importinggenerate_textwithout 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →