C.W.K.
Stream
Lesson 01 of 07 · published

Provider Port — When and Where

~16 min · provider-port, abstraction, architecture

Level 0Observer
0 XP0/64 lessons0/13 achievements
0/150 XP to next level150 XP to go0% complete

Two valid patterns, one wrong move

For multi-provider systems, two patterns are valid: (1) provider port — a narrow ABC over the streaming API, every provider implements it, downstream code stays neutral; or (2) canonical-shape variants — pick one provider as canonical, others specialize downstream. Both ship; both survive. The wrong move is generalizing prematurely on day one with one provider.

cwkPippa picked variants

cwkPippa's adapter ABC is intentionally narrow — only the streaming-turn boundary. Routes, store, and frontend assume Claude shape. ChatGPT, Gemini, and Ollama brains are variants, not peers. This is Rule 2 from cwkPippa's architecture: cost is absorbed downstream, never pushed upstream. The result is fewer abstractions and more direct code, at the cost of needing a canonical decision early.

When to choose port instead

If your providers are genuinely interchangeable (same capabilities, same cost shape, swappable mid-traffic), a strict port pays off — you can A/B providers, fail over cleanly, and bring up new ones without touching downstream code. If they are not interchangeable (one has prompt caching, another has agent harness, another is local-only), variants will serve you better.

Principle: Concrete first, abstract second. The day you have two providers is the day you decide. Day one with one provider is too early.

Code

cwkPippa-style narrow ABC·python
from abc import ABC, abstractmethod
from typing import AsyncIterator, Any

class Adapter(ABC):
    """The ONLY abstraction in the system.
    Routes, store, and frontend assume the response shape this returns."""

    @abstractmethod
    async def stream_turn(
        self,
        *,
        conversation_id: str,
        messages: list[dict[str, Any]],
        system: str,
    ) -> AsyncIterator[dict[str, Any]]:
        ...

# claude.py is the canonical implementation;
# codex.py / gemini.py / ollama.py SPECIALIZE downstream — they do not abstract upward.
Strict port (the alternative pattern)·python
# Only correct if providers truly are interchangeable.
class ModelProvider(ABC):
    capabilities: dict  # advertise prompt_caching, files, agent, etc.

    @abstractmethod
    async def generate(self, req: GenerateRequest) -> GenerateResult: ...
    @abstractmethod
    async def stream(self, req: GenerateRequest) -> AsyncIterator[Event]: ...
    async def count_tokens(self, req: GenerateRequest) -> int: ...  # optional

# Caller branches on capabilities and falls through to common surface.

External links

Exercise

For your project, write one paragraph: which pattern (port or canonical-shape variants), and the one specific feature or constraint that drove the choice. If you have not picked yet but have two providers, that is the decision waiting.
Hint
If your justification is 'flexibility', that is not a justification — every pattern claims that. Name a feature or a deploy story.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.