A governed AI platform: a model gateway, a job queue, and an observability stack, running on local Kubernetes.
It routes requests across local models by policy, falls back when one is unhealthy, meters and bills every caller, runs long jobs through a queue that survives a dead worker, traces and alerts on all of it, deploys itself from this repository through Argo CD, and has been broken on purpose three times to find out what it actually does.
Every line of code here was written by an AI (Claude Code) under a human director who wrote none of it and instead verified every phase by hand — the project is as much a study in governed AI orchestration as in platform engineering.1 That discipline shows in the test suite: 658 tests, 0 failing, of which roughly 56% cover the product and 44% cover the governance tooling that keeps the AI honest — the claim checkers, the rehearsal harness, and the drill instruments.2
A note on the codes. Short IDs such as
M3,Q-33,D-26,ENV-04andS109appear throughout — they are stable references into the decision ledger. See Notation for what each prefix means.
- What it does · Milestones
- See it running — architecture and live captures
- Install and run
- Notation — what the
M,Q,D,ENV… codes mean - Where the record lives
- How this project is built
- The two published numbers
- Honest limits · License
The gateway exists and works: it forwards OpenAI-format chat completions to a
local Ollama, carries the undocumented reasoning field and the token counts
through intact, maps every upstream failure to a documented status, and
survives its upstream being killed and restarted without a restart of its own.
It has one provider and binds to localhost.
M3 adds identity and metering. Callers are named by API key, with secrets held
outside the tracked configuration; each key carries a short-window rate limit
and a long-window token quota, refused with distinguishable 429s and an honest
Retry-After. Every request writes a cost record to Redis carrying two figures
that are never both simply "cost" - what was consumed and what the caller is
answerable for - plus a count of attempts whose cost could not be observed at
all. Until a key is declared the gateway authenticates nothing and limits
nothing, and says so on every response.
M4 adds asynchronous work. Jobs are submitted to a Redis Stream and run by a separate worker process through the gateway's own provider path, so a queued completion is routed, retried, priced and recorded exactly like a direct one. Idempotency is the server's rather than this project's. A worker that dies mid-job leaves its entry pending, a recovery sweep reclaims it, and a job that has been delivered too many times is moved to a dead-letter stream with its reason attached instead of being retried forever.
The submission path fails CLOSED while completions fail OPEN, on the same Redis, in the same process - accepting a job you did not store is losing it while telling the caller you kept it. Two workers can occasionally finish the same job, and the acknowledgement decides which answer is served: the loser keeps its own cost record, marked, because the tokens were really spent. Cost records are append-only per attempt, so a redelivery cannot erase what an earlier attempt consumed.
The platform was built in nine milestones (M0–M8), each with its own contract
(docs/decisions/STEP-NN-*.md), its own plain-English concept doc (concepts/),
and closed only on evidence the Director approved.
| # | Milestone | What it added |
|---|---|---|
| M0 | Scaffold | Repository, charter, tooling, Ollama and two local models |
| M1 | Gateway core | FastAPI service serving OpenAI-format /v1/chat/completions, forwarding to Ollama behind a provider abstraction |
| M2 | Routing + fallback | Policy-based model selection (capability, cost weight, latency), health checks, a fallback chain, and bounded retries |
| M3 | Quotas + cost | Per-key rate limits and token quotas in Redis, per-request cost accounting, and distinguishable 429s |
| M4 | Queue + workers | Async jobs on Redis Streams consumer groups, a reclaim sweep, idempotency, and a dead-letter stream |
| M5 | Observability | OpenTelemetry GenAI traces, Prometheus metrics, Grafana dashboards, and alert rules |
| M6 | Kubernetes | Dockerized services, a kind cluster, a Helm chart, health probes, and rolling update plus rollback |
| M7 | GitOps | Argo CD syncing the Helm chart from this repo, with drift detected and reconciled |
| M8 | SRE drills | Killing a worker, Redis, and an Ollama backend on purpose, each with a runbook and an RCA, plus a capacity note |
docs/showcase/ presents the whole system wired together and proof of it running — captured live, with timestamps, on 2026-08-22. It has three parts:
- How it connects — an architecture diagram and a walk through the data path for a synchronous completion, an async job, and the GitOps reconcile loop.
- The full system running — a real completion (routed, priced, answered by a model) and an async job carried through the queue and worker, end to end.
- Observability — five completions moving
switchyard_requests_totalfrom 0 to 5 in Prometheus, with cost recorded, plus the M5 Grafana dashboards. - GitOps self-heal — Argo CD reverting hand-planted drift on the live cluster automatically, with no human, in under a second, with the Argo CD UI captured in docs/showcase/gitops-argocd.md.
The raw, timestamped captures behind those pages are under
evidence/showcase/.
Everything is local — Python 3.14, Docker, and Ollama. No paid APIs are used at any point in this project.
switchyard is packaged as one container image with two entry points (gateway and worker), published on the GitHub Container Registry:
docker pull ghcr.io/mohdsaifhussain/switchyard:1.0.0The fastest way to run it is the demo stack, which brings up Redis and the gateway plus a worker wired to Ollama on your host. On first run it builds the image locally from the Dockerfile at the repository root, or you can pull the published image above.
# Prerequisite: Ollama running with the two models pulled
ollama pull qwen3:4b
ollama pull llama3.2:1b
# Bring up the stack (auth disabled in the demo)
docker compose -f deploy/demo/docker-compose.yml upThen:
curl http://localhost:8000/livez # 200 — a heartbeat independent of Redis/Ollama
curl http://localhost:8000/health # names every dependency and the posture toward it
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"auto","messages":[{"role":"user","content":"Say hello in one word."}]}'Full prerequisites and the operator guide — verifying it, turning authentication on, and the two deliberate "refuses to start" cases — are in deploy/demo/README.md.
Throughout this repository, short IDs reference the append-only decision ledger,
DECISIONS.md (browsable at docs/ledger/). An
ID, once issued, always refers to the same entry — so Q-33 means the same
ruling wherever it appears.
| Prefix | Meaning |
|---|---|
M0–M8 |
The nine milestones; each has a contract at docs/decisions/STEP-NN-*.md |
Q-NN |
A ruling — a decision the Director made, with its conditions |
D-NN |
A defect found, with how it was found and how it was resolved |
DEV-NN |
A deviation from a contract, with its reason |
OPEN-NN |
An open question the project knows it has not answered |
ENV-NN |
An environment trap — a property of this machine or toolchain |
S-NN |
A source: a URL fetched, when, and what it settled (SOURCES.md) |
Nine milestones, each closed on evidence in this repository that a stranger can re-read. 9 milestones closed; the charter's map is complete. Released as v1.0.0 on 2026-08-22, with the container image published to GHCR. This remains a self-operated portfolio project — no production traffic — and the honest limits below stand unchanged.
| Path | What it holds |
|---|---|
CLAUDE.md |
The charter: roles, hard rules, milestone map |
docs/decisions/STEP-NN-*.md |
One contract per milestone: objective, deliverables, rulings, outcome |
DECISIONS.md |
Append-only ledger: every ruling, defect, and deviation, by ID |
docs/ledger/ |
A generated, browsable view of the ledger, one page per namespace |
SOURCES.md |
Every URL fetched, when, and what it settled |
docs/showcase/ |
The system wired together, and proof of it running |
docs/HARVEST.md |
Candidate rules earned here, queued for the skill at project end |
RUNBOOK.md |
Playbooks to investigate and runbooks to act, written from the three drills after they ran |
docs/CAPACITY.md |
This machine's measured ceilings, what hit each one, and what the platform did |
docs/rca/ |
One root-cause analysis per drill, on the Google SRE postmortem fields |
CHANGELOG.md |
Keep a Changelog format |
concepts/ |
One plain-English concept doc per milestone, no code |
evidence/ |
Command transcripts: what was actually run, and its real output |
tools/check_claims.py |
Verifies counts stated in prose against the ledger |
tools/verify_phase.py |
One command for every mechanical phase-close check |
tools/config_refusals.py |
Shows the shipped config refusing four silent misconfigurations |
tools/prepush_audit.py |
What leaves the machine, and where it lands: history scan plus a visibility read |
tools/check_cluster.py |
Cluster state against the tracked kind config: reachable, Ready, unskewed, undrifted, no residue |
Every milestone runs the same way, and nothing self-closes: a research pass
from official documentation logged in SOURCES.md, then the build, then a
green test suite, then evidence captured in evidence/, then a concept doc in
concepts/ written in plain English. The Director's approval closes the
milestone.
Two rules do the heavy lifting:
- Official sources only. Every technical decision is researched from the vendor's own documentation and cited by URL with the date it was fetched. Where the documentation is silent, the question is settled by a measured probe and recorded as a measurement, never as an assumption.
- Every gate proves it can fail. A check that has only ever passed is a decoration. Each gate carries a negative control that must refuse and a positive control that must accept, with a distinct reason per failure mode.
The suite currently has 658 tests, 0 failing. Of those, 55 are integration tests that exercise a real running Ollama or a real running Redis, and 6 read the live Kubernetes cluster; each of those skips, with its reason printed, when what it needs is absent.
The count is passed plus skipped, and that is a weaker claim than a passed-only figure — so it is not written that way. The passed-only figure read 555 with the cluster up and 551 with it down when it was measured on 2026-08-19, against the suite as it stood that day; those two readings are what ruling Q82(b) was decided on, and they are left exactly as measured rather than restated at today's size (Q-135). The count says how big the suite is and that nothing in it is failing; it does not say every test ran. Counting only passes made the published number move with whatever happened to be running on the machine that day rather than with the suite, so a stopped container turned the check red for a reason that had nothing to do with the code.
That number is not maintained by hand: tools/check_claims.py re-derives it
from an actual pytest run and fails if this line disagrees with it. A real
failure still turns it red — any failing or erroring test makes the count
unreadable rather than merely smaller, so a skip can never mask a failure. It
has now caught this line drifting thirty-two separate times.
That count is now verified rather than asserted. Its previous wording put a word between the noun and "closed", which the claim checker's pattern never matched - so the one number describing this project's progress was the one number nothing checked. The obvious rewording was worse and was tested before being rejected: the checker reads the digit nearest the noun, so a phrasing that names the total immediately before it would have had the tool verifying the wrong figure. This sentence deliberately carries no digit before that noun, because an illustration is indistinguishable from a claim to anything that reads by pattern (D-17).
This section is standing. It is not removed as the project matures, and it is narrowed only when something genuinely narrows.
-
This platform is self-operated on one machine. A single Windows laptop: 15.6 GB RAM, an NVIDIA RTX 4060 Laptop GPU with 8188 MiB of VRAM, an Intel i7-13650HX. Every ceiling in this project is that machine's ceiling.
-
No production traffic, ever. All load is self-generated. Nothing here has served a user, and no number in this repository should be read as a production measurement.
-
SRE drills are self-inflicted. The failures exercised in M8 are ones this project causes on purpose. They are rehearsals, not incidents, and finding that a rehearsed failure was handled says nothing about an unrehearsed one.
-
Model quality is never evaluated here. Routing decisions are made on size, latency and health. This project makes no claim about which model answers better, because it has not measured that and will not.
-
Latency figures are single observations on a laptop under unknown background load. They are recorded as observations. They are not benchmarks and are not cited as benchmarks.
-
One provider. Provider-level routing is not exercised. The gateway has a provider abstraction and routes between targets, but every target sits behind the same local Ollama, so what is actually proven is model routing behind that abstraction. Calling it provider routing would overstate what has been run. The standing evidence that a second provider can be added without editing the request handler is a test that registers one and resolves it; that test passes, and it is not the same thing as having run two. Recorded by ruling Q-20 and carried forward until a second provider lands.
-
This gateway authenticates nothing until its first key is declared. With no
[keys.*]table inswitchyard.toml, every request is served unauthenticated. That is deliberate - a freshly cloned repository has to be usable before it is understood - and it is made loud rather than quiet: startup logs it,/healthreports"auth": "disabled", and every response carriesx-switchyard-auth: disabled (no keys configured)so a caller learns the truth from any reply. This is the one line that closes it:[keys.your-name] limits = "standard"
then set
SWITCHYARD_KEY_YOUR-NAMEin the environment, or add the secret toswitchyard.secrets.toml, which is gitignored. A declared key with no secret anywhere refuses startup rather than silently never matching. Recorded by ruling Q-33. -
Some costs are unknowable, and are recorded as unknown rather than as zero. When an upstream attempt fails it reports no token usage at all: a 5xx carries no usage block, and a timeout carries no body. But a timeout may mean the upstream generated and billed an entire completion nobody ever saw. So a failed attempt is counted as consuming an unknown amount, labelled wherever that count appears, and never priced at zero. Recorded by ruling Q-34.
-
A caller can achieve double the configured rate across a window boundary. Rate limiting is fixed-window, and this weakness is not merely documented - it is run: three requests per minute became six in a two second span, in
evidence/M3/d6-boundary-burst.md. A sliding window would prevent it at the cost of more state per caller. Recorded by ruling Q-27. -
A refused request still counts against its rate window. The counter increments before the check, per redis.io's own documented pattern. For a rate limit this is arguably correct - it is the caller ignoring the refusal that the backend needs protecting from - but 429s are not free.
-
A caller can overshoot their quota by one request. Consumption cannot be predicted before the call - M2-D2 measured a fourfold spread in completion tokens for identical input, and 86x to 357x between targets - so the check admits on what is already recorded and the charge records what was used. A quota bounds what a caller may START, not what they may consume.
-
A queued job that runs twice charges the quota once, and the second run's tokens are recorded but never billed. A worker can be reclaimed while it is still alive, so two workers occasionally finish the same job; only one answer is served and only that one is charged. The tokens the other burned are real, and they appear in that attempt's own cost record marked as a duplicate serve - so an operator reconciling total consumption sees them, and the caller's allowance does not drain for them. The reason is that the second run is system-caused: the recovery sweep's bet going wrong is the operator's cost, not the caller's, and a quota that drained because a reclaim threshold guessed wrong would punish a caller for the platform's uncertainty. Answer not served, cost recorded, quota not charged. Recorded by the ruling on OPEN-15.
-
When Redis is unavailable, requests are served unlimited and unrecorded. Deliberate: a gateway that stops serving because its BOOKKEEPING is down has turned one outage into two. It is never silent - every response says limits were not enforced,
/healthreports the store unreachable, and the blind window is logged with a start and an end so reconciliation knows exactly which period is unaccounted. The per-request numbers from that window are lost permanently. Recorded by ruling Q-28. -
Counters survive a graceful Redis stop, and do not survive a kill. This was stated backwards until it was measured, and the correction is kept rather than quietly edited (D-26). Redis snapshots to disk by default;
docker stopsendsSIGTERM, which triggers a blocking save, and the container reloads that snapshot on start. What is lost is everything written since the last snapshot when Redis is killed rather than stopped -docker kill, a crash, or power loss - and everything, without exception, if the container is removed, because no volume is mounted and the snapshot lives in the container's own writable layer. Quotas are therefore durable enough to be misleading: durable across the stop-start an operator does deliberately, and not across the failure they do not. And that durability is a property of the default configuration, not of stopping: redis.io saves on shutdown only "if at least one save point is configured", so a Redis run as a pure cache withsave ""loses everything on the same graceful stop. -
With no keys declared, nothing is limited either. Limits attach to a caller, and an unauthenticated request has none to attach to. The open-by-default state announced on every response covers this too.
-
Health information is never fresher than its cache window. Routing checks a target on demand and caches the answer briefly, so the first request after a target dies still pays that failure. Where a fallback exists the caller pays it as latency rather than as an error, which softens the cost without removing it. Recorded by ruling Q-21.
MIT — see LICENSE. Copyright © 2026 Mohd Saif Hussain.
Footnotes
-
Governed AI orchestration is the method this project is built under: a written charter, per-milestone contracts, and verification gates so that no claim is trusted wider than its evidence. The rules are in
CLAUDE.md, and the reasoning behind each decision is in the ledger,DECISIONS.md. ↩ -
Measured by test-function count across
tests/: roughly 232 functions exercise the product (gateway, routing, quotas, queue, worker, telemetry, the Helm chart) and roughly 180 exercise the governance tooling (the claim checker, phase verifier, rehearsal harness, and drill instruments). ↩