I built an operating system for AI.
It humbled me.
This is not a post about prompts. It's about what happens after the prompt works: the outage you didn't plan for, the bill nobody can explain, and the question "why did it say that?" when your whole career is on the line. I rebuilt the layer under every AI app I've ever shipped. Here's what happened, including the bug that made me look like a fool to my own test suite.
Chapter OneThe week it all broke
Let me start with the moment my relationship with AI engineering changed. Not a success story. An outage.
A provider I depended on had a partial failure. Nothing dramatic, just enough degraded responses to poison my app. And here's the thing that kept me staring at the ceiling that night: there was no seam anywhere in my code where a second provider could have existed. The SDK call sat directly in the request path like a load-bearing wall. To add a fallback, I would have had to rebuild the room.
That same week, two more walls came down. A stakeholder asked me which prompt version and which model produced a specific bad output, and roughly what it cost us. I opened my editor, scrolled through prompts pasted between functions like comments, and realized the honest answer was: nobody knows, and nothing was designed to know.
And then I watched an agent burn through tokens retrying a failing tool call. No budget. No cap. No event history. It failed silently until someone noticed the bill.
The models were fine that week. Every single failure was mine. They were plumbing failures, accounting failures, custody failures. The boring kind of failure that software engineering solved decades ago for databases and message queues. The AI industry just hadn't gotten the memo yet.
So I made an unreasonable decision. I would stop assembling features and build the substrate properly: a gateway, a knowledge pipeline, an agent engine, a control plane, and a shared contract library. Five packages. Production rules. If a claim couldn't be proven by a test, it didn't go in the README.
This is the story of building it, told honestly, including the parts where the system outsmarted me.
Chapter TwoOne rule before line one
Before writing code, I wrote down how the project could fail. Not runtime failures. Project failures: phases that end when enthusiasm ends, scope that grows because nothing stops it, quality that decays because "done" means different things on different days.
The countermeasure was one rule, enforced ruthlessly:
A phase doesn't exit when its code exists. It exits when its evidence exists.
Every phase got exit criteria written before implementation, each mapped to named test files. Gateway couldn't close until three providers failed over live. Agent runtime couldn't close until I killed a worker mid-run and proved it resumed without repeating itself. RAG couldn't close until a red-team suite failed to cross tenant lines.
The rule changed my relationship with testing. Tests stopped being the tax you pay after building and became the proof-of-reality layer the whole project stands on. When I later claimed "one trace follows a request across four services," that wasn't marketing copy. There is a test file that asserts the actual bytes of the trace header. Hold that thought, because that test is also where this story gets embarrassing.
Execution itself was agentic. I ran an AI dev-engine as a pair, but the division of labor mattered more than the tooling: the agents wrote code fast; the criteria decided what counted. On the days everything looked finished, the criteria said otherwise, and the criteria were right. More than once.
If you take one practice from this post: write the definition of done first, make it testable, and let it argue with you. It's the cheapest senior engineer you'll ever hire.
Chapter ThreeHiring a bouncer for my API keys
Every AI app has a moment where a user's words become someone else's API call. That moment is where money leaks, outages begin, and costs become unexplainable. So that's where the build started: a gateway, a service whose entire job is to stand between your application and the model providers and impose order.
The pitch sounds simple. Proxy chat completions across Groq, Mistral, Gemini, SiliconFlow, NVIDIA NIM, all speaking OpenAI's wire format. The devil is in what "impose order" means:
| When reality happens | What the gateway does instead |
|---|---|
| A provider starts returning 500s | A circuit breaker trips after consecutive failures, half-open probes test recovery, traffic walks a deterministic fallback chain. Proven E2E by executing two of three mock providers mid-request. |
| Someone discovers your endpoint | API-key auth with hashed storage, tenant claims, per-tenant model allowlists, sliding-window rate limits in Redis. Burst-tested with an exact 200/200/429 sequence and proper quota headers. |
| The finance team asks what a customer costs | Tokens metered per request into ClickHouse, reconciled against provider-reported usage to within zero tokens on fixture runs. Estimator drift is tracked separately, honestly, as reconciliation_delta. |
| The same prompt arrives ten thousand times | A tenant-scoped input cache answers instantly, and provider-native prompt-cache discounts are metered too, so the savings show up in the ledger instead of vanishing. |
The detail that separates a proxy from a product: streaming under chaos
SSE passthrough runs over hijacked sockets with true backpressure, keep-alives, and client-disconnect propagation. The test suite kills upstream connections mid-stream deliberately. Clients receive exactly what made it through, no phantom completions, and the metering sink still records partial usage correctly.
There's also a latency gate: sub-15ms added overhead non-streaming, sub-5ms TTFB impact streaming. When your product is a middleman, its weight is the metric. The benchmark runs with a subtracted baseline and a stated CI-noise allowance because fake precision is worse than honest error bars.
Under the hood: one request through the gateway
POST /v1/chat/completions
Bearer ax_..."] --> B["resolveTenant
keyHash → tenant claims"] B --> C["Zod schema validation
on wire body"] C --> D["experiment engine
sticky bucket(salt, sessionKey)"] D -->|"arm: model override /
prompt template"| E D --> E["rate limit check
sliding window in Redis
→ x-ratelimit-* headers"] E --> F{"input cache hit?
(tenant-scoped)"} F -->|"yes"| G["serve cached completion
+ meter it, then done"] F -->|"no"| H["extract W3C traceparent
open gateway.chat server span"] H --> I["circuit breaker picks chain:
model's provider → defaultChain"] I --> J["adapter.complete / stream
SSE hijack or buffered JSON"] J --> K["meter row → ClickHouse
(async batched sink)"] style A fill:#141B2B,stroke:#9AA4B8,color:#E8ECF4 style D fill:#1A1608,stroke:#FFC46B,color:#FFC46B style F fill:#0F1D16,stroke:#46E3C8,color:#46E3C8 style G fill:#0F1D16,stroke:#46E3C8,color:#46E3C8 style H fill:#101A2C,stroke:#5CC8FF,color:#5CC8FF style K fill:#101A2C,stroke:#5CC8FF,color:#5CC8FF
Two details worth pausing on. The experiment resolves at step three, before the allowlist check, so an arm's model override passes through the exact same tenant policy as a client-chosen model; there is no privileged bypass path. And the metering sink is async and batched, so billing writes never sit inside the request's critical path.
My favorite piece hides in the economics. Most teams treat caching as an optimization footnote. Here it's a first-class subsystem with its own dashboard, its own hit-rate metrics, and its own honest limitation recorded in the docs: the exact-match tier is process-local until the multi-worker scale-out lands. Saying what isn't done is part of the feature.
The stack, precisely: what the gateway is actually built from
Runtime: Node 20, TypeScript, Fastify 5 (chosen over Express for the gateway specifically because route-level hooks map cleanly onto the auth → validate → route pipeline, and Fastify's logger hooks are where the redaction engine attaches). State: ioredis for the sliding-window limiter and cache tier, pg for the hashed API-key store. Contracts: Zod schemas from @tanvir1971/core validate every wire body; a request that fails schema validation never reaches a provider adapter.
Provider layer: one ProviderAdapter interface with an openaiCompatible.ts implementation covering Groq, Mistral, Gemini's OpenAI-compat endpoint, SiliconFlow, and NVIDIA NIM, plus a dedicated Anthropic adapter for its differing wire format. A classify.ts module maps provider error classes to breaker decisions, so a 429 from one provider is not treated as a 500 from another. Streaming: sse.ts hijacks the raw socket for passthrough; sseUsage.ts parses usage deltas out of the stream for metering without buffering the whole completion.
Metering: async batched sink into ClickHouse, with a reconciliation_delta column comparing the estimate against provider-reported usage. The sink is deliberately off the request's critical path; the chaos suite proves the gateway survives ClickHouse being down entirely.
Chapter FourTeaching software to remember (without leaking)
Retrieval looks easy. Embed documents, search vectors, return chunks. Every tutorial ends there. Production begins where the tutorials end, with three questions nobody asks until the audit:
Whose memory is this?
In a multi-tenant system, the scariest bug class is silent crossover: customer A's document fragment surfacing in customer B's answer. My rule became structural: tenant scope comes from verified credentials and is enforced by the data layer itself. A caller cannot pass filters that cross tenants, because the mandatory claim filter is not the caller's to modify. Then I hired an adversary. Nine red-team tests try forged signatures, mismatched credentials, crafted filter payloads, cache probing across boundaries. All of them fail closed. That suite passing matters more to me than any benchmark score in this post.
How do we answer the same question twice, for free?
A two-tier cache. Exact matches short-circuit entirely. Paraphrases hit a similarity tier backed by Qdrant. The interesting part isn't the hit, it's proving the hit is free: the test asserts zero embedding-provider calls on the hit path via transport mocking. Anyone can claim a semantic cache saves money. This one has a test that counts API calls.
Is retrieval actually good?
"Feels accurate" is not an engineering property. A golden set of topics drives a recall@10 regression gate wired into CI, scoring every build. Currently 1.00 against a threshold of 0.9. If a chunker change quietly degrades retrieval, CI turns red before any human notices something feels off. Chunkers themselves are property-tested for lossless reassembly, because if chunking silently drops characters, nothing downstream, including your evals, can detect it.
The plan originally specified Celery workers and the Unstructured parser library. Reality: FastAPI background tasks plus native markdown/HTML/text parsing covered v1's needs completely. Rather than pretend, the tracker records this as an accepted deviation with the pluggable seams left in place. Changing your mind cheaply is what architecture records are for.
Chapter FiveThe agent that could not be killed
Agents are where demos go to die. In a demo, the loop completes. In production, the worker gets OOM-killed mid-plan, the tool hangs forever, the webhook receiver is down for maintenance, and someone asks why the agent "did that." This package exists to answer those moments.
Kill it. It comes back.
Agent execution is event-sourced: every step, plan, tool call, observation, persisted as it happens. The signature test literally kills the worker mid-run and brings up a fresh one, which replays from the last event and finishes the job without re-running the planner or duplicating a single step. Underneath sits plain BullMQ rather than Temporal, a deliberate bet: event sourcing bought most of the durability at a fraction of the operational cost. The deferral is documented with a revival trigger, not forgotten.
Under the hood: how a run survives its own death
agent-exec (tenant lanes) participant W1 as worker #1 participant DB as run_events
(Postgres, append-only) participant T as sandboxed tool participant W2 as worker #2 Q ->> W1: claim job (idempotency key) W1 ->> DB: append PLAN W1 ->> T: tool.call #1 T -->> W1: observation W1 ->> DB: append TOOL_CALL + OBSERVATION W1 -x W1: ✖ worker killed mid-run Note over W1,DB: crash: nothing in memory survives,
but every transition was already appended Q ->> W2: same job redelivered
(idempotency key dedupes) W2 ->> DB: read event log DB -->> W2: resume after last completed step Note over W2: planner NOT re-invoked
no duplicate tool side effects W2 ->> T: tool.call #2 (next pending step) T -->> W2: observation W2 ->> DB: append OBSERVATION + DONE W2 -->> Q: job complete style W1 fill:#241014,stroke:#FF6B6B,color:#E8ECF4 style DB fill:#101A2C,stroke:#5CC8FF,color:#E8ECF4 style W2 fill:#0F1D16,stroke:#46E3C8,color:#E8ECF4
The subtle part is idempotency at every boundary: job IDs are colon-free so Redis cluster slots them safely, redelivered jobs dedupe on their idempotency key, and each tool call carries one too, because replay means some effects may have landed before the crash. Durability is mostly bookkeeping discipline, not exotic infrastructure.
Untrusted code meets a locked room
Tools execute inside an isolated VM with a serialized bridge only. No host handles cross the boundary. CPU-time caps kill infinite loops. Heap caps kill bombs. Module loading, network access, filesystem egress: blocked by construction, and asserted by a red-team suite I genuinely enjoyed writing. Every escape I could imagine, turned into a test that must fail.
Webhooks: physics vs promises
Delivery is at-least-once; that's physics and you don't fight physics. Observation is exactly-once: receivers dedupe by HMAC signature, timestamp replay protection, and event ID. The integration test delivers the same event three times and asserts it was processed once, with tampered deliveries rejected before recording anything. Failed deliveries land in a dead-letter queue with a replay CLI, so "temporarily down" never means "permanently lost."
The quiet feature that saves agents from their own context windows
Context assembly packs system prompt, history, and retrieved chunks into a model's window with priority ordering and truncation markers, unit-tested against every registered model's context length. Unglamorous. Also the difference between an agent that degrades gracefully at the token ceiling and one that crashes at 128k.
Chapter SixThe witness stand
Four services produce behavior. The fifth produces evidence. I think of the ops plane as the witness stand where the rest of the system explains itself, and it permanently changed how I ask questions about AI systems:
| The question | What answers it now |
|---|---|
| "Why did it say that?" | One W3C trace ID reconstructs the full journey: gateway attempt, retrieval hits, sandbox executions, in Jaeger or raw ClickHouse. |
| "Which prompt is live, and what changed?" | Prompts are immutable semver artifacts with validated template variables and dev→staging→prod promotion. Published versions can't change. Only new versions can exist. |
| "Is the new prompt actually better?" | An eval engine scores prompt-version × model combinations against golden datasets, and a CLI gate blocks promotions on regression. Eval is a checkpoint, not a decoration. |
| "Should we roll it out to everyone?" | Deterministic sticky A/B splits between arms (prompt versions or model overrides), reported once per session key, summarized with 95% confidence intervals and win probabilities. |
The experiment engine carries my favorite design constraint in the whole project: the gateway resolves assignments locally from polled rules, so when the control plane goes down, requests degrade to "everyone gets control" rather than "nobody gets completions." Compare that to the security paths, sandbox escapes, forged webhooks, cross-tenant reads, which fail closed by design. Knowing which paths may fail open and which may not is half of reliability.
The stats are honest too: seeded Monte-Carlo win probabilities from wide posteriors, arms with no data can't win by default. Small-n experiments produce humble conclusions, on purpose.
Chapter SevenFive repos, one handshake
The smallest package carries the most architectural weight. Types for requests, chunks, usage, errors, tenants. Zod schemas mirroring the environment contract so startup fails fast on misconfiguration. HMAC signing shared identically by webhooks and inter-service auth. The telemetry module every service instruments with. Protobuf definitions gating breaking changes behind major versions.
Why does this matter? Because drift between services is a contracts problem before it's a deployment problem. Without the shared package, service A's "validation_failed" becomes service B's mystery 500. With it, breaking changes require a major version bump in public, with a changelog. It felt like overhead for about a week. Then it felt like gravity.
@tanvir1971/core
├── types/ requests · chunks · usage · tenants · errors
├── config/ Zod schemas (startup fails fast, loudly)
├── crypto/ signPayload / verifySignature (one format everywhere)
├── telemetry/ OTel SDK + Gen-AI span conventions ← plot twist lives here
└── proto/ axiom/v1 (the source of truth)
Some decisions I sweated over, recorded so future-me can judge present-me:
| The fork | Where I landed, and the sentence that decided it |
|---|---|
| Rust or Node for the gateway? | Node. Shared ecosystem with the contracts beats theoretical throughput. Benchmark-before-rewrite keeps the door honest. |
| Temporal or BullMQ? | BullMQ plus event sourcing. Ninety percent of the durability, ten percent of the ops burden, documented revival trigger. |
| Qdrant or Redis for the similarity cache? | Started Redis. Switched to Qdrant mid-build: it already did per-tenant filtered search natively. Deleting a consistency-bug class beats defending an earlier choice. |
| Stripe now or later? | Test-mode only, GA deferred. Metering lands fully formed in ClickHouse either way. Zero external spend for v1, as a recorded constraint, not a hope. |
Chapter Seven and a HalfLift the hood
The story so far has been deliberately narrative. This interlude is for the reader who now wants to see the machine: what runs where, what talks to what, and how one request actually moves through all of it.
Everything that runs, on one page
| Package | Language / framework | Key libraries | Owns |
|---|---|---|---|
axiom-gateway :3000 | TypeScript / Fastify 5 | ioredis, pg, js-tiktoken, Zod | Auth, rate limits, failover, caching, metering |
axiom-rag-pipeline :8000 | Python / FastAPI + Pydantic v2 | psycopg3, pypdf, httpx | Ingestion, chunking, embeddings, retrieval |
axiom-agent-runtime :5000 | TypeScript / Express 4 | BullMQ 5, isolated-vm 7, pg | Event-sourced runs, sandboxed tools, webhooks |
axiom-ops-observability :4000 | TypeScript / Fastify 5 | Prisma 6, Ajv, pg | Traces, prompt registry, evals, A/B stats, billing |
@tanvir1971/core (npm) | TypeScript | Zod, OTel SDK (@opentelemetry/sdk-trace-base), pg | Types, config schemas, HMAC crypto, telemetry, protobuf contracts |
Versions are pinned in each package manifest; the constraint that shaped this stack is that only @tanvir1971/core is published, so every shared dependency upgrade is a single semver-gated decision.
input cache · experiments
failover router"] end PROV["providers
Groq · Mistral · Gemini · NIM
(failover chain)"] RAG["axiom-rag-pipeline :8000
Python · FastAPI"] AGENT["axiom-agent-runtime :5000
BullMQ · isolated-vm · webhooks"] CH[("ClickHouse
metering rows")] QD[("Qdrant
vectors")] RD[("Redis
cache · queues")] PG[("Postgres
run events")] OPS["axiom-ops-observability :4000
traces · prompt registry · evals
A/B stats · Grafana"] OPG[("Postgres/Prisma
prompts · datasets")] APP --> GW GW -->|"OpenAI-compatible"| PROV GW -->|"retrieve"| RAG GW -->|"dispatch"| AGENT GW -->|"async batch"| CH RAG --- QD RAG --- RD AGENT --- RD AGENT --- PG GW -.->|"W3C traceparent
every hop"| OPS AGENT -.-> OPS CH --> OPS OPS --- OPG classDef service fill:#101A2C,stroke:#5CC8FF,color:#E8ECF4,stroke-width:1.5px classDef store fill:#141B2B,stroke:#9AA4B8,color:#E8ECF4 classDef ext fill:#0F1D16,stroke:#46E3C8,color:#E8ECF4 classDef ops fill:#1A1608,stroke:#FFC46B,color:#E8ECF4 classDef app fill:#241032,stroke:#A48FFF,color:#E8ECF4 class GW,RAG,AGENT,OPS service class CH,QD,RD,PG,OPG store class PROV ext class APP app
A few deliberate choices hide in this picture. Every arrow that crosses a service boundary carries W3C traceparent, which is why a single trace ID can reconstruct an entire journey later. Every store has exactly one job: ClickHouse for immutable telemetry rows, Postgres for stateful artifacts (prompts, run events, experiment records), Qdrant for vectors, Redis for anything ephemeral. No store is doing two jobs; no service reaches past another to touch a store it doesn't own.
Anatomy of one request
keyHash → tenant claims G->>G: t+2ms experiment?
bucket(salt, sessionKey) G->>R: t+3ms sliding-window check alt over limit R -->> G: deny → 429 + retry-after else allowed G->>G: t+4ms cache lookup (tenant-scoped hash) alt cache hit G-->>C: serve cached completion + meter it else miss Note over G: t+5ms extract traceparent,
open server span G->>P: t+6ms route via circuit breaker Note over P: 5xx? breaker records failure,
next provider in chain P-->>G: completion + reported usage (t+600ms) G->>CH: meter row: tokens reconciled vs estimate G-->>C: SSE stream or JSON (t+602ms) end end Note over G,CH: async: spans flush to ops plane
→ Jaeger-compatible queries classDef client fill:#241032,stroke:#A48FFF,color:#E8ECF4 classDef gw fill:#101A2C,stroke:#5CC8FF,color:#E8ECF4 class C client class G gw
The whole proxy tax lives in the first few milliseconds of bookkeeping. Everything after routing is the upstream model's own latency, untouched.
Where the hard guarantees actually live
| Guarantee | Mechanism under the hood |
|---|---|
| Tenant isolation | Credentials → claims → mandatory filters applied at the data layer; callers supply intent, never scope |
| Sandbox containment | isolated-vm with CPU-time + heap limits, serialized bridge only, no host handles crossing the boundary |
| Run durability | Append-only event log per run in Postgres; replay resumes from last completed step with idempotent tool calls |
| Webhook integrity | HMAC-SHA256 signature + timestamp window + event-id dedupe; DLQ with signed replay CLI |
| Cost accountability | Per-request meter rows in ClickHouse, estimator vs provider-reported drift tracked as a first-class column |
| Trace correlation | Propagator registered once in the shared library; server span per request; byte-asserted by test at every hop |
Why five data stores instead of "just use Postgres for everything"?
Fair challenge. Postgres genuinely could store vectors, queues, and telemetry; people do. The split buys three things: each store's superpower where it matters (ClickHouse ingesting millions of immutable telemetry rows cheaply, Qdrant doing filtered vector search natively, Redis at microsecond queue latency), independent scaling and failure domains (a Redis restart never touches billing history), and honest operational simplicity per concern, since tuning Postgres for OLAP workloads while also serving transactional writes is its own specialty.
The tradeoff is real too: more moving parts at boot, hence the compose stack and the smoke check that verifies every health endpoint. For a small self-hosted install, starting with Postgres-only and adopting stores one at a time behind the same interfaces would be a defensible path, and the interfaces are drawn so that path stays open.
That's the machine. Now back to the story, because the best part, the part where the machine fooled me, comes next.
Chapter EightThe bug that left no trace
Every long build has one bug that humbles you. Mine arrived in the final week, wearing a green checkmark.
The last open deliverable was correlation: prove one trace ID follows a request from client through gateway to provider. Traces were flowing. Spans were landing in ClickHouse. Dashboards rendered. Everything looked observably, verifiably fine.
Then I wrote the proof test: capture the outbound request at a mock provider and assert it carries a valid W3C traceparent descended from the inbound one.
expect(outbound.traceparent).not.toBe("");
// → received: ""
Empty. Not malformed. Not missing sometimes. Empty.
The root cause read like a riddle. OpenTelemetry ships a default propagator called NoopTextMapPropagator. Its job is to do nothing, gracefully. Registering a tracer provider, which I had done, does not register a propagator. Nobody tells you this. No warning fires. Every "inject" call I made was injecting context into a void, and the void accepted it politely.
And there was a second layer to the embarrassment: even with a real propagator, my gateway injected from an empty ambient context. It never extracted the inbound headers in the first place. So even "fixed," traces would have started chains from scratch, severing the very correlation the whole exercise existed to create.
Observability failures are unique: the system works perfectly except for the part that tells you how it works. Nothing errors. Nothing logs. Your traces are simply emptier than you believe, possibly forever. Conventional tests cannot catch them, because conventional tests don't assert on observability. Only a byte-level assertion on the contract itself does. Test the traceparent, not the dashboard.
The fix went in at both layers. The shared telemetry initializer now registers the W3C propagator explicitly, so every service inherits correct behavior by existing. The gateway extracts inbound context and opens a server span per request, so clients that send no tracing headers still get a valid chain. The test turned green and stayed in the suite as permanent evidence. The bug that left no trace now leaves a very specific one.
Honorable mentions from the same campaign: a not-found handler shadowed by parameterized routes (caught by scaffold test), colons in job IDs breaking Redis cluster slotting (fixed, commented, unfixed-able again), and a stray local process squatting on port 4000 during verification, immortalized as an honest port deviation in the tracker rather than silently papered over.
Chapter NineWhat I'd tell you over coffee
Strip away the code, and here is what four months actually taught me.
Evidence is a culture, not a phase. The exit-criteria rule felt bureaucratic for exactly three days. Then it caught a failing assumption I would have shipped proudly. Definition-of-done written first, testable, arguing back: cheapest senior engineer available.
Adversarial design is cheaper than adversarial incidents. The security suites, sandbox escapes, tenancy attacks, webhook forgery, shipped cleanest of anything in the project. Not because I'm clever. Because they were hostile from the first commit instead of bolted on after the first scare.
Cost is a first-class requirement. Metering with reconciliation deltas reshaped design decisions far from billing. Cache-hit paths proven to avoid embedding calls. Prompt-cache discounts surfaced instead of silently absorbed. If you can't attribute spend per tenant per feature, you're not running a product, you're sponsoring one.
Prompts are artifacts, not strings. Immutable versions, validated variables, promotion gates. Once "which prompt produced this?" has a real answer, eval scores mean something, rollbacks become possible, and prompt changes stop being folklore.
Defer loudly. Temporal, Celery, Unstructured, Stripe GA: every cut is written down with the condition that revives it. "We'll revisit if X happens" is strategy. Silence is just debt with better branding.
And document the deviations. The port conflict, the swapped cache backend, the forward-looking metrics scrapes. Readers trust documentation that admits limits. I trust engineers who write them.
What different readers should take away
The asides below are aimed at different readers on purpose. If you build products on AI, the five questions are your procurement checklist. If you write AI systems, the patterns are the transferable seams and gates. If you own the budget, the CTO list is what to demand in a design review before writing a check.
1. Which providers does it fall back to when one dies?
2. Can it tell you what one customer interaction cost?
3. Can it show you why a specific bad answer happened?
4. What prevents two customers' data from mixing?
5. Who approves a prompt change before users feel it?
Silence on any of these isn't a platform gap. It's uninsured risk, waiting to be priced.
If you're an AI engineer or ML expert
Adapters + error classification: one provider interface, with a classifier that maps provider errors to breaker semantics (a 429 is throttling, not failure) so fallback chains behave deterministically.
Structural tenancy: tenant scope injected from verified JWT claims as a mandatory data-layer filter. Callers supply intent, never scope. This deletes the entire "caller passed the wrong filter" bug class.
Event sourcing over workflow engines: append-only run events + idempotency keys at every boundary gave 90% of Temporal's durability on plain BullMQ.
Byte-level contract tests: assert the traceparent header itself, the exact rate-limit header sequence, zero embedding calls on the cache-hit path. Dashboards can lie; assertions can't.
Shared contract package: cross-service drift is a types problem before it's an ops problem. One npm package, breaking changes gated behind majors.
If you're a CTO or CAIO
Exit criteria as artifacts: every phase closed on written, test-mapped criteria, not vibes. Ask to see them.
A risk register with reopen triggers: not "is it secure?" but "which risks are accepted, why, and what event reopens the decision?"
Cost attribution per tenant per feature: if spend can't be attributed, unit economics are folklore.
Adoption surface: can a team pull one package without swallowing the platform? Monoliths get evaluated; packages get adopted.
The deviation log: a team that writes down what it cut, and when it would un-cut it, is a team you can trust with production.
If you build AI systems yourself
Seams: provider adapters behind one interface; a vector-store interface making Qdrant-vs-Pinecone a config choice; a rules-polling experiment engine that degrades to no-op; event-sourced runs so durability doesn't require a workflow engine.
Gates: recall@k in CI. Reconciliation deltas on usage. Escape suites that must fail closed. A traceparent assertion. None glamorous. Together: the difference between having features and being a system.
Chapter TenThe part most posts skip
Credibility comes from what you admit, so here's the ledger.
Not load-proven at scale. Formal k6 profiles, 1k concurrent streams, webhook storms, are Phase 5. Today's performance claims come from targeted benchmarks with stated methodology, not sustained production traffic.
You operate it. This is self-hosted infrastructure. Single-node compose by default; retention, tuning, and pager duty are yours. The Prometheus scrape targets declared in this repo were written ahead of the services exposing /metrics exporters, and the dashboards say so plainly.
Some plan features shipped as deviations. Background tasks replaced Celery workers. Native parsers replaced Unstructured. Eval scorers run in-process rather than as subprocesses. Each recorded with rationale. Deviation logs are how a project stays honest with its own roadmap.
And it's not done. Guardrails middleware, a full docs site, Helm charts, and v1.0 tags remained at the end of Phase 4. All five landed in the sequel, and the honest close-out of every one of them, including what stayed accepted rather than fixed, is in The shift from AI toolkit to ecosystem.