
Hey guys, Mr. Technology here.
I just spent $40 debugging a tool schema against gpt-4o over a weekend. That is a rounding error at a startup; that is my rent as a freelancer. The fix is a 35-line router that makes your existing OpenAI and Anthropic SDK calls transparently point at a local Ollama instance during development — one import, one env var, no call-site changes.
LLM_DEV_MODE=ollama # off = real providers (default) OLLAMA_BASE_URL=http://localhost:11434/v1 LLM_MODEL_MAP=gpt-4o=llama3.1:8b,gpt-4o-mini=llama3.1:8b,claude-sonnet-5=qwen2.5:14b
Production never sets LLM_DEV_MODE — the router is a no-op there.
Drop at src/dev_router.py:
"""dev_router.py — transparent LLM provider swap for development."""
import os, functools
_MODE = os.environ.get("LLM_DEV_MODE", "off").lower()
def _parse_map(s):
out = {}
for pair in (s or "").split(","):
if "=" not in pair: continue
model, tag = pair.split("=", 1)
provider, _, name = model.partition("/")
if name: out[(provider.strip(), name.strip())] = tag.strip()
return out
_MAP = _parse_map(os.environ.get("LLM_MODEL_MAP", ""))
_BASE = os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434/v1")
def remap(provider, model):
return _MAP.get((provider, model), model)
if _MODE == "ollama":
import openai
_Orig = openai.OpenAI
class _Routed(_Orig):
def __init__(self, *a, **kw):
if "base_url" not in kw:
kw["base_url"], kw["api_key"] = _BASE, "ollama"
super().__init__(*a, **kw)
openai.OpenAI = _Routed
try:
import anthropic
_OrigA = anthropic.Anthropic
class _RoutedA(_OrigA):
def __init__(self, *a, **kw):
if "base_url" not in kw:
kw["base_url"], kw["api_key"] = _BASE, "ollama"
super().__init__(*a, **kw)
anthropic.Anthropic = _RoutedA
except ImportError:
pass
def _wrap(provider, original):
@functools.wraps(original)
def w(self, *a, **kw):
if "model" in kw: kw["model"] = remap(provider, kw["model"])
return original(self, *a, **kw)
return w
_Routed.chat.completions.create = _wrap("openai", _Orig.chat.completions.create)The subclass trick patches the constructor. The _wrap on the last line catches clients built before the import.
ollama serve & # start daemon ollama pull llama3.1:8b
import dev_router # ← one new line, top of entrypoint
from openai import OpenAI
client = OpenAI()
resp = client.chat.completions.create(
model="gpt-4o", # ← real name, gets remapped
messages=[{"role": "user", "content": "say hi in 4 words"}],
)
print(resp.choices[0].message.content)Hello there, friend.
gpt-4o was rewritten to llama3.1:8b, base URL pointed at localhost, cost $0.00. Streaming and tool-calling work the same way.
Function-calling: Pin to tool-capable tags — qwen2.5:14b is the 2026 sweet spot. Older models silently ignore tools= and emit raw JSON in content.
Context length: Llama tokenizes differently from Claude. Set num_ctx=8192 in your Modelfile or expect truncation past ~6K input tokens.
import dev_router flips the whole app to local OllamaLLM_DEV_MODE=ollama controls it; unset in productiongpt-4oThirty-five lines. One import. Ship Monday.
— Mr. Technology