ADR-004: Template Method Base Class for OpenAI-Compatible Providers¶
Status: Accepted Date: 2026-03-15 Deciders: Core maintainers
Context¶
The 4 provider implementations totaled 1776 lines. OpenAI (421 lines) and Ollama (456 lines) shared ~95% of their code: same SDK (OpenAI Python client), same message formatting, same tool schema mapping, same response parsing. The only differences were:
- Error message strings ("OpenAI" vs "Ollama" + connection hints)
- Pricing (calculate_cost() vs 0.0)
- max_tokens vs max_completion_tokens (OpenAI-specific for newer models)
- Tool call ID handling (Ollama generates UUIDs for missing IDs)
Gemini and Anthropic use completely different SDKs with different APIs, so they share almost nothing with the OpenAI pair.
Decision¶
Create _OpenAICompatibleBase (ABC) in providers/_openai_compat.py using the Template Method pattern. OpenAI and Ollama inherit from it and override only the varying parts via hook methods:
_get_token_key(model)— token parameter name_calculate_cost(model, prompt_tokens, completion_tokens)— pricing_get_provider_name()— for UsageStats_wrap_error(exc, operation)— error message formatting_parse_tool_call_id(tc)— ID extraction/generation
Do NOT create a shared base for Gemini or Anthropic.
Rationale¶
-
High duplication, low divergence: OpenAI and Ollama had 5 small differences in 400+ lines of identical code. The Template Method pattern isolates exactly these differences as overridable hooks.
-
Gemini/Anthropic excluded deliberately: They use
google-genaiandanthropicSDKs respectively — the message formats, response structures, and streaming APIs are fundamentally different. Forcing them into a shared base would require so many abstract methods that the base class would be hollow. -
ABC, not Protocol: The base class contains real implementation (~400 lines of shared logic). It's an implementation inheritance hierarchy, not an interface contract.
typing.Protocol(ADR-001) remains the public interface. -
Private class:
_OpenAICompatibleBaseis prefixed with underscore and not exported. It's an internal implementation detail. Users interact withOpenAIProviderandOllamaProvider.
Consequences¶
- Positive: OpenAI provider went from 421 to 86 lines (-80%). Ollama from 456 to 126 lines (-72%).
- Positive: Bug fixes to response parsing, message formatting, or streaming now happen in one place.
- Positive: Adding a new OpenAI-compatible provider (Azure OpenAI, Groq, Together AI) requires only
__init__+ 5 template method overrides. - Negative: Debugging requires following the Template Method dispatch. Stack traces show
_OpenAICompatibleBase.complete()rather thanOpenAIProvider.complete(). Mitigated by clear method naming and short override methods. - Negative: Gemini and Anthropic remain standalone. Their duplication (complete/acomplete are similar within each) is accepted as the cost of SDK divergence.