FifeRouter

1 September 2026 · routing config

Adding a model should be a config edit, not a deploy of new code

Four artifacts, kept separate on purpose. Adding a model is a row and an id; adding a route is four sentences. Nothing is retrained and no concept file moves.

Four files decide everything the router does:

config/pool.yaml       what can be reached, and on what terms
config/concepts.yaml   the routable units, authored by people
config/policy.yaml     concept -> ranked candidates + objective
config/providers.yaml  base URLs, and which env var holds each key

They could be one file. They are four because of what has to change independently.

Adding a model

A row in pool.yaml:

- id: vendor-balanced
  provider: anthropic
  model: claude-sonnet-5
  residency: vendor
  tier: mid
  context_window: 200000
  capabilities: [tool_use, json_mode, long_context, streaming]
  price_in: 0.30
  price_out: 1.50

And its id in whichever policy rows should consider it. That is the entire change. No concept file moves. Nobody re-authors a taxonomy because the model catalogue grew, and nothing is retrained, because the classifier compares against exemplar sentences rather than against a learned mapping from prompts to models.

Everything the hard filters gate on lives in the pool row — residency, tier, context window, capabilities — because those are properties of the model. Put them in the policy and you would repeat them per concept and eventually disagree with yourself.

Adding a route

A concept with a description and three or more exemplars, plus a policy row. The model catalogue is untouched.

The two changes are orthogonal, which is the whole point of the split: the question "what kinds of work exist" and the question "what can we reach" have different answers, different owners, and different rates of change.

Validated against each other, at load

An id in a policy row that names no model in the pool is an error at startup, not a silent drop to the next candidate:

{"status":"ready","models":5,"concepts":5,
 "default_concept":"general.assist",
 "catalogue_fingerprint":"c16768ae114fc540"}

/readyz fails while the four artifacts have not loaded and agreed with each other, so a bad config takes the instance out of rotation instead of serving subtly wrong routing. A typo in a model id is a container that will not come up — which is loud, early, and much better than a policy row that quietly has one fewer candidate than its author believed.

The fingerprint is there so two instances can be compared: same catalogue or not, one string.

Config, not a database

The four artifacts are baked into the image. A routing change is a deploy with a diff somebody reviewed.

The alternative — a table, an admin UI, live edits — is genuinely attractive and we are not doing it. Three reasons, in increasing order of importance.

It would put a lookup on the request path, against a 5% routing budget that exists to keep network calls off it.

It would make "what was the routing policy last Tuesday" a question about database history rather than about git log.

And it would mean a residency constraint could change without a review. That is the one that settles it: residency: in_infra on money-path work is a compliance decision, and a compliance decision that can be edited in a web form by whoever is on call is not one.

The cost, honestly

Adding a model needs a deploy, and deploys are slower than a form submission. That is a real cost, paid on a change that happens rarely, in exchange for a routing policy that is reviewable, revertible and attributable.

It is the same trade as authoring concepts rather than learning them, and the same reasoning: for the parts of this system that encode a rule somebody is accountable for, being fast to change is worth less than being hard to change by accident.


← All posts