Command Palette

Search for a command to run...

Hectal
PHASE 2Beginner ~19 min· topic 8 of 9

Topic 2.8

One Interface, Many Providers

In one line

Give your code one tiny ChatModel interface and put every real client, fake and recorder behind it. Features then never learn which provider they use, tests need no key, and switching providers becomes a configuration change.

Think of it like this

A wall socket. Your lamp, laptop and kettle all have the same plug shape, and none of them knows or cares whether the power comes from coal, solar or a generator. If the power company changes, nothing in your house is rewired. The socket is the interface; the power stations are the providers.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Interface
The agreed list of methods a component offers. Anything that provides them can be swapped in.
Adapter
A class that makes one provider's API fit your interface.
Fake
A stand-in implementation with scripted behaviour, used in tests and demos.
Decorator (wrapper)
A class with the same interface as the one it wraps, adding behaviour such as logging or retries.
Factory
A function that builds the right implementation from configuration.
Dependency injection
Passing the model into a function instead of creating it inside, so callers choose which one.
Test double
Any stand-in used instead of a real dependency in a test: fake, stub, recorder.

Step by step

01Step 1: write the feature against the interface

make_title(model, note) takes any object with complete. It builds the messages and returns the text, with "Untitled" when the reply is blank. It contains no URLs, keys or provider names.

Step 1: write the feature against the interfacediagram
Rendering diagram…

02Step 2: write the fake

The fake pops a scripted reply. With it the feature is tested without a network. The second config returns " " (blank), which exercises the Untitled branch; a real model rarely does that on demand.

03Step 3: write the real-shaped client

HttpModel builds the JSON body from Topic 2.1, sends it through a transport function and reads choices[0]. In the example the transport is a fake server, so it runs offline; in production it is a real HTTP call (Topic 2.9 shows one).

04Step 4: wrap with a recorder

Recorder forwards the call and remembers the number of messages and the reply text. Tests use it to assert what the feature sent. The same idea, extended, becomes logging and metrics.

05Step 5: choose by configuration

make_model turns a config dictionary into the right object, optionally wrapped in a recorder. The loop at the bottom tries four configs: two fakes, the HTTP-shaped client, and an invalid provider that fails with a clear message.

06Step 6: keep the interface small

Resist adding every provider option to complete. Add what the features need, put provider-specific settings in the adapter's constructor or config, and add new methods only when a second feature needs them.

Try it yourself

  1. 1

    Predict the output

    Before running, predict the four result lines and whether each Recorder line appears. Which configs have record turned on?

  2. 2

    Add a provider

    Add a provider "echo" whose model returns the user's note unchanged. Where do you add it, and do you need to touch make_title?

  3. 3

    Test the cut-off case

    Add a fake reply "" (an empty string) as a new config. What does the feature return, and why is that better than returning an empty title?

Code & diagrams

One interface: a fake, an HTTP-shaped client and a recorder New tab

The HTTP-shaped client talks to a fake transport, so the example is offline. The pattern is the same one the real client in Topic 2.9 uses.

Sign in to run this example in your browser.

Expected output

fake -> Rain Expected In Pune
  recorded 1 call(s), 2 messages in the first
fake -> Untitled
http -> Title: Rain is expected in Pune this week
  recorded 1 call(s), 2 messages in the first
satellite -> config error: unknown provider: satellite

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Default silently on a bad config

In the Python make_model, replace the final else: raise ValueError(...) branch with else: model = FakeModel(["?"]), so an unknown provider quietly gets a fake, then run the example again.

terminal
$ Run
── what you'll see ──
fake -> Rain Expected In Pune
recorded 1 call(s), 2 messages in the first
fake -> Untitled
http -> Title: Rain is expected in Pune this week
recorded 1 call(s), 2 messages in the first
satellite -> ?

Myth vs fact

Myth

An interface is over-engineering for a small project.

Fact

It is ten lines, and it makes tests free and provider changes cheap. Add it from the first call.

Myth

With the same interface, any provider gives the same results.

Fact

The code is swappable, the model is not: answers, formats and failure modes differ, so re-run your evaluations after a switch (Phase 9).

Myth

A framework's abstraction replaces my own interface.

Fact

Frameworks change quickly. A thin interface of your own keeps framework types out of your features, whichever library you use underneath.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Define capabilities explicitly (supports_tools, max_context_tokens) on the adapter, so features can ask before using them and routers (Topic 12.6) can pick a model by need.

  • ▸

    Contract-test every adapter with the same test suite: same inputs, same expected reply shape, same error classes. A new provider then passes or fails the suite instead of surprising you in production.

  • ▸

    Record real replies once (a cassette or snapshot file) and replay them in tests. This keeps tests realistic without cost, and the recording exposes shape changes when you refresh it.

Remember this

  1. 1

    Why this exists. Without an interface, requests.post("https://api...") and provider field names are scattered through your code. Switching provider means editing dozens of places; testing means hitting the network and spending money; adding a retry or a log means touching every call. With one narrow interface, all of that changes in one place.

  2. 2

    The interface. Keep it as small as the feature needs: complete(messages, max_tokens) -> reply, where the reply has text and finish_reason (add token counts when you need cost, Topic 2.9). Your own types, not the provider's: the SDK's response classes stay inside the adapter. Streaming and tools can be added later as separate methods or richer replies (Phases 5 and 7).

  3. 3

    Many implementations. A fake returns scripted replies, so tests are free, instant and can simulate cut-off, empty and failing replies (Topic 0.7). A real client (HttpModel) builds the request, sends it and parses the reply. A recorder wraps any model and remembers each request, so a test can check the exact messages that were sent. The example runs all three behind the same complete.

  4. 4

    Decorators compose. Because every wrapper has the same interface as what it wraps, you can stack them: Logged(Retrying(Cached(Real))). Each does one job (logging, retries, caching) and can be tested alone. The recorder in the example is the simplest decorator; Topic 2.9 adds a logger and cost meter, and later phases add retries, caches and fallbacks the same way.

  5. 5

    Switching by configuration. A small factory (make_model) reads a config value (an environment variable or a file) and returns the right implementation. Running the app with provider=fake gives a demo with no key; provider=http calls the real service. An unknown provider fails at start-up with a clear message, not at the first user request. Note the example's satellite config.

  6. 6

    What the interface cannot hide. Providers differ in features (structured output, tool format, caching, context size) and in quality, so a swap still needs evaluation (Phase 9). Keep the interface to what is common and add optional capabilities explicitly. Do not leak provider names into the feature code, and do not make the interface so wide that every adapter must fake what it cannot do.

Explain it without notes

01

What problem does a ChatModel interface solve?

02

How do fakes, recorders and real clients fit together?

03

What can't an interface hide when switching providers?

Practice

01

Write a class Counter that wraps any model and counts how many times complete was called (calls attribute), returning the wrapped model's reply.

02

Write make_model(config) that returns a FakeModel for {"provider": "fake"} and raises ValueError("unknown provider: ...") otherwise.

Trade-offs

  • ↔

    A narrow interface is easy to implement and test, but hides provider features until you extend it.

  • ↔

    A wide interface exposes more, but every adapter must support or fake each part.

  • ↔

    Wrappers keep each concern small, at the cost of a deeper call stack to read when debugging.

Done when you can

  • Done when your feature code contains no provider names, URLs or keys.

  • Done when you can run the feature against a fake in a test.

  • Done when you can add logging or a recorder without changing the feature.

  • Done when you can switch providers with a configuration change, and failing config stops start-up.