Punith Prabhakar Reddygonokie.dev · 開発者
All writing随筆
AI Engineering · 5 June 2025 · 1 min read

Building a Provider-Agnostic LLM Layer: The Adapter Pattern in Practice

How to integrate multiple LLM providers without letting their APIs leak into your application logic.

The lock-in problem

Every LLMLLMLarge language model, the kind of model behind ChatGPT and Claude. provider has a different API. Different authentication, different request schemas, different response formats, different streaming protocols, different error codes. If you build your application directly against one provider's SDK, you're one API change, one pricing shift, or one outage away from a significant refactor.

The adapter pattern solves this — but implementing it for LLMs has specific challenges worth documenting.

The interface contract

Start by defining what your application actually needs from an LLM provider, independent of any specific API:

class LLMClient(Protocol):
    async def complete(
        self,
        messages: list[Message],
        max_tokens: int,
        temperature: float,
    ) -> CompletionResponse: ...
    
    async def stream(
        self,
        messages: list[Message],
        max_tokens: int,
    ) -> AsyncGenerator[str, None]: ...

Everything provider-specific lives inside concrete adapter classes that implement this interface. Your application never sees an Anthropic client or an OpenAI client — only an LLMClient.

The streaming problem

Streaming is where adapter implementations get genuinely difficult. Anthropic, OpenAI, Groq, and Ollama all have different SSE or chunked response formats, different event types, and different ways of signalling stream completion.

The cleanest solution I found: each adapter's stream implementation is an async generator that normalises provider-specific events into a uniform string stream. The adapter handles all the provider-specific parsing internally and yields only the text delta to the caller. The application code is identical regardless of which provider is underneath.

Error normalisation

Providers also have different error taxonomies. Rate limit errors, context length errors, and content filter errors all need to map to your application's error types — not bubble up as provider-specific exceptions that your application logic has to handle case by case.

Build an error normalisation layer inside each adapter. Define your own exception hierarchy (RateLimitError, ContextLengthError, ContentFilterError) and translate provider exceptions to your types at the adapter boundary.

The result

When we built Document Intelligence MVP with this architecture, switching between Anthropic, OpenAI, Groq, Gemini, and Ollama was a single config change. No application logic changed. The adapter pattern paid for the upfront design cost in the first week.