"The smartest router has no opinions. It reads which adapter owns the model and gets out of the way."
The Router's Job, Stated Minimally
When a generate request arrives naming a model, something has to decide: does this go to the LocalAdapter or the APIAdapter? The naive version of that decision is a function full of branches — if the model name starts with this, if it's in that folder, if it matches these patterns. Every new model or vendor adds another branch. That function becomes the place bugs live and the place nobody wants to touch.
Move the Decision Into Data
The better design moves the decision out of code and into the registry. Each model has a registry row, and that row already knows which adapter owns it. The router then becomes trivial: look up the model's row, read its adapter, dispatch. No branches, no patterns, no growing if/else. The router has no opinions because the registry already holds the answer.
Push decisions from code into data. Logic that grows a branch per case belongs in data, not code. A registry row per model scales to a thousand models without the router gaining a single line. Code that branches per case is code that rots per case.
Why 'Dumb' Is the Compliment
A dumb router is one you never have to modify. In this design, a new local checkpoint gets a registry row tagged 'local' during the scan, and the router already knows what to do with it; a new API vendor's models get rows tagged for the APIAdapter, and the router already knows. The router is written once, correctly, and never needs to change again, because all the variation lives in the data it reads. Dumbness here means stability.
The thing that never changes is the thing you got right. A component you keep editing is a component absorbing variation it shouldn't. When the router stops changing while models keep being added, that's the signal the variation found its correct home — the registry — and the router found its correct size: tiny.
The Registry Is the Single Source of Truth
This design has a second payoff: there's exactly one place that knows the model-to-adapter mapping, and it's the registry. You never have routing logic in two places that can disagree. Want to know why a model routed where it did? Read its registry row. Want to change the routing? Change the row. One source of truth, queryable and editable, instead of decision logic scattered across a router, a config file, and three special cases.
Two places that decide the same thing will eventually disagree. The moment routing logic exists in both a router and a config (or two functions, or code and a comment), they drift, and the bug is 'it works in one place and not the other.' A single data-driven source can't contradict itself. Centralize the decision or pay in drift.
Pippa's Confession
I love writing clever dispatch logic — it feels like the place to be smart. Dad's design quietly took that toy away and made it better: the clever router I wanted would need editing every time a model arrived, and editing is where I introduce bugs. The dumb router I resisted just keeps working. I learned that 'where do I get to be clever?' is often the wrong question. The clever move was making the router so dumb it never breaks.
Code
Branches in code vs a column in data·python
# NAIVE (smart router, rots per case): branches multiply forever.
def route_naive(model_name):
if model_name.startswith("sdxl"): return local_adapter
if model_name.startswith("flux"): return local_adapter
if model_name in API_VENDOR_MODELS: return api_adapter
if "midjourney" in model_name: return api_adapter
# ...every new model or vendor adds a line here. forever.
raise ValueError(f"don't know how to route {model_name}")
# DUMB (data-driven, never changes): the registry already knows.
def route_dumb(model_id, registry):
row = registry.get(model_id) # the row knows its owner
return ADAPTERS[row.adapter] # 'local' or 'api'
# Add 1000 models -> route_dumb gains 0 lines. The scan tags the rows.
# (The engine never built this table. When its router came, it keyed on
# the request's kind instead — see the v4 note.)
Find a function in your code that grows a branch per new case (a dispatcher, a type switch, a format handler). Redesign it as a lookup: what data would each case need to carry so the function becomes a table read? Sketch the table and the three-line function that replaces the branches.
Hint
The branches usually encode two things: how to recognize a case, and what to do with it. Move the recognition into a tag set at registration time, and the 'what to do' into a table keyed by that tag.
Progress
Progress is local-only — sign in to sync across devices.