One client, three providers

Because OpenAI, OpenRouter, and Fireworks all speak the same protocol, one class handles all three. The trick is moving the base_url, api_key, and default model into config, then writing a tiny registry that maps a model prefix to the right instance.

utils/llm_provider.py
python
from openai import AsyncOpenAI
from providers.base import LLMProvider, Message, ChatResult

class OpenAICompatibleProvider(LLMProvider):
    def __init__(self, api_key: str, base_url: str | None = None, name: str = 'openai'):
        self.client = AsyncOpenAI(api_key=api_key, base_url=base_url)
        self.name = name

    async def chat(self, messages: list[Message], model: str) -> ChatResult:
        payload = [m.model_dump() for m in messages]
        response = await self.client.chat.completions.create(
            model=model,
            messages=payload,
            temperature=0.3,
        )
        usage = response.usage
        return ChatResult(
            text=response.choices[0].message.content or '',
            model=f'{self.name}/{model}',
            prompt_tokens=usage.prompt_tokens if usage else None,
            completion_tokens=usage.completion_tokens if usage else None,
        )

Notice what moved into config: api_key, base_url, and a name used for logging. Everything else stays generic, which is exactly why three providers share one class.

router.py
python
import os
from providers.openai_compat import OpenAICompatibleProvider

_providers: dict[str, OpenAICompatibleProvider] = {}

def _get_or_create(name: str, api_key_env: str, base_url: str | None) -> OpenAICompatibleProvider:
    if name not in _providers:
        _providers[name] = OpenAICompatibleProvider(
            api_key=os.environ[api_key_env],
            base_url=base_url,
            name=name,
        )
    return _providers[name]

def resolve(model: str) -> tuple[OpenAICompatibleProvider, str]:
    if model.startswith('openrouter/'):
        return _get_or_create('openrouter', 'OPENROUTER_API_KEY', 'https://openrouter.ai/api/v1'), model.removeprefix('openrouter/')
    if model.startswith('fireworks/'):
        return _get_or_create('fireworks', 'FIREWORKS_API_KEY', 'https://api.fireworks.ai/inference/v1'), model.removeprefix('fireworks/')
    return _get_or_create('openai', 'OPENAI_API_KEY', None), model

The registry is the only place that knows which prefix maps to which backend. New providers get a new elif and nothing else in the codebase changes.

Matching exercise: Model strings to backends

Loading practice…

AI prompt: Try it: add a fourth provider

Loading practice…