A8 (Touchstone)
apps/a8 serves the A8 eval model (a8-<major>-<minor>) on /eval/v1, in four
dialects: OpenAI Chat Completions, OpenAI Responses, Anthropic Messages, and a
native route. Serving is deterministic, and no LLM touches the request
path. Teaching arrives as an
expected field on the same wire, and a background worker fits the model and
promotes it. Every request is scoped to the organization on its API key, so one
deployment holds a model per organization over a published shared baseline.
This page covers what is unusual about a8 as a project in this monorepo. It deliberately stays thin; the details live where they can't drift:
| For | See |
|---|---|
| The public wire contract | /docs/a8-reference on the marketing site (apps/web) — that page is the spec |
| Working in the app | apps/a8/README.md |
| How it deploys | apps/u22a8-api/README.md in u22a8-infra |
It is the repo's one Python app
Everything else here is TypeScript. a8 is Python because its engine is numerical code, where low-order float differences change a verdict. Node has no maintained BLAS binding.
It integrates through Nx rather than yarn: apps/a8 has a project.json but
no package.json, so Nx discovers it while yarn workspaces ignore it. That
is why adding a8 required no changes to the other apps' Dockerfiles.
nx lint a8 # ruff check + format --check
nx test a8 # the full offline suite — no database, no network, no API key
nx run a8:test-db # adds the tests needing PostgreSQL + pgvector
nx serve a8 # PORT=… to override the default 8000
nx run a8:worker # the fit worker
Everything shells out to uv, which is installed in the devcontainer and in CI.
apps/a8/.env, not the root .envNx injects the workspace-root .env into every task, and a8 needs
DATABASE_URL to point at PostgreSQL while web's local Prisma setup uses
SQLite. One root value can't be both. Nx loads project-level env files, so a8's
belong in apps/a8/.env (already git-ignored).
Moving a verdict is a declaration
When a change can move a verdict, the provenance tokens in
engine/provenance.py have to move with it — SELECTION_LAW when fitting
changes (every model refits), SERVE_LAW when serving can shift a verdict
without a refit. They are
declared claims about behaviour, not source hashes, so a docs commit does not
churn every client's system_fingerprint.
What the offline suite actually covers
The published contract, and nothing about how good the model is:
tests/api/— the wire: status taxonomy,x-request-id,usage, boot configuration, and the four dialects driven through the OpenAI and Anthropic SDKs.tests/engine/test_contract.py— the promises a caller is given: banking as a pure side effect, the min_accuracy ladder, pins and horizons, the shape of an abstention.tests/engine/test_artifacts.py— a DB-backed read must match its in-RAM twin.tests/persistence/— model and training-data round trips, where a regression loses data silently rather than loudly.tests/engine/vectors.json— a cassette for the one upstream call, so the suite runs with no network.
Verdict quality and the learning curve are the end-to-end benchmark's job: it measures them on real corpora, which is the only setting where those numbers mean anything. The suite's toy corpus cannot speak to model quality.
llm marker does not mean "needs the network"It means "exercises the upstream call", and those tests replay from the
cassette offline by default — they are the fidelity gate, and they run in
CI. -m 'not llm' skips them and leaves a green suite that proved nothing; the
CI filter is -m 'not db'. Use --run-llm only to re-record the cassette.
Abstention is a feature
a8 answers only at an accuracy it has earned. Asked for a promise it cannot
keep, it returns a 200 with an abstention object rather than guessing or
erroring. A cold or under-taught model abstaining is correct behaviour, not a
bug — see /docs/a8-abstention on the marketing site.