Quality gates / quality (pull_request) Waiting to run
The counters had an endpoint and no way to read them without jq. Adds a pure formatter plus a CLI that prints the app, queue, and model boundaries. The formatter is a function of the snapshot and the three frozen vocabularies, so a caller-supplied failure label cannot reach the terminal: unknown classes are simply not printed. Refusal paths are explicit — 404 means the route is off because no token is configured, 403 means the token was rejected — and the token is never echoed in the report or an error line. RED: tests/model-metrics-report-format.test.js failed with ERR_MODULE_NOT_FOUND before src/model-metrics-report.js existed; the refusal-path test failed with a generic 'answered 404' before the 404 branch was written. Tests: npm test 298/298, npm run test:metrics-report 8/8, syntax and diff gates clean, real-Chromium ui acceptance green with the privacy disclosure visible.
112 lines
5.6 KiB
Markdown
112 lines
5.6 KiB
Markdown
# Privacy-safe model health counters
|
|
|
|
Issue [#19](https://forge.alexanderwhitestone.com/git/stackchain/timmy-talking-turd/issues/19).
|
|
This is the operator surface for "is the worker healthy?", and it is deliberately the smallest one
|
|
that answers the question. The journal stays the only place a person's bowel data lives.
|
|
|
|
## Endpoint
|
|
|
|
```
|
|
GET /api/metrics
|
|
Authorization: Bearer $TIMMY_METRICS_TOKEN
|
|
```
|
|
|
|
- `TIMMY_METRICS_TOKEN` unset → **404**. A default deployment has no metrics route at all; nothing is
|
|
exposed by forgetting to configure something.
|
|
- Token set, header missing or wrong → **403**, with a body that contains no counters.
|
|
- Token set and matched → **200** with the snapshot below.
|
|
|
|
The snapshot is read from process memory. It survives no restart, is written to no log, and is sent
|
|
to no third party.
|
|
|
|
## What is in the snapshot
|
|
|
|
```json
|
|
{
|
|
"schema": 1,
|
|
"generatedAt": "2026-11-01T10:00:00.000Z",
|
|
"app": { "uptimeMs": 0, "analyzeRequests": 0, "refusedBeforeModel": 0 },
|
|
"queue": { "inFlight": 0, "limit": 4, "atCapacity": false, "accepted": 0 },
|
|
"model": {
|
|
"outcomes": { "suggestion": 0, "abstained": 0, "failure": 0 },
|
|
"failureClasses": { "ingress_refused": 0, "rate_limited": 0, "disabled": 0,
|
|
"provider_unreachable": 0, "provider_rejected": 0,
|
|
"schema_invalid": 0, "timeout": 0, "unexpected": 0 },
|
|
"abstentionRate": null,
|
|
"readiness": { "phase": "checking", "ready": false, "coldLoadMs": null },
|
|
"latency": { "count": 0, "meanMs": null, "maxMs": 0,
|
|
"bucketsMs": { "1000": 0, "2000": 0, "5000": 0, "15000": 0, "60000": 0 } }
|
|
}
|
|
}
|
|
```
|
|
|
|
The three blocks are the three boundaries, and each counts only what it owns:
|
|
|
|
| Block | Owns | Does not own |
|
|
|---|---|---|
|
|
| `app` | Every `POST /api/analyze` the process saw, and the ones refused before any model work (bad bytes, per-device budget, analysis disabled). | Queue occupancy or model answers. |
|
|
| `queue` | Slots handed out and released, and the configured concurrency ceiling. | Whether the request succeeded. |
|
|
| `model` | Answers (`suggestion` / `abstained`), classified failures, readiness phase, and model-work latency. | Anything decided before a slot was taken. |
|
|
|
|
`abstentionRate` is `abstained / (suggestion + abstained)`; it is `null` until the model has answered
|
|
at least once. A failed request is **not** an abstention — an outage must not look like a cautious
|
|
model.
|
|
|
|
`latency.bucketsMs` is cumulative: `"5000"` counts every sample at or below 5 s. `coldLoadMs` is the
|
|
interval from process start to the first observation in which the pinned model was resident; a worker
|
|
that flaps back to `offline` keeps the original figure.
|
|
|
|
`readiness.phase` uses the same vocabulary the browser uses for the AI affordance (`checking`,
|
|
`disabled`, `offline`, `loading`, `ready`), and the value is produced by the same
|
|
`visionReadiness()` function the app renders, so a metric and a screen cannot disagree.
|
|
|
|
## What can never appear in it
|
|
|
|
The only strings this module stores are members of three frozen lists — outcomes, failure classes,
|
|
and readiness phases — plus a fixed latency-bucket edge set. Anything else is bucketed, not copied:
|
|
|
|
- an unrecognised outcome becomes `failure`;
|
|
- an unrecognised failure class becomes `unexpected`;
|
|
- an unrecognised readiness phase becomes `checking`.
|
|
|
|
Therefore no note text, no image bytes, no image hash or fingerprint, no subject key, no caller
|
|
address, and no session or cookie value can reach a label. `tests/model-metrics.test.js` asserts this
|
|
by feeding hostile strings into every label position, and
|
|
`tests/metrics-endpoint.acceptance.test.js` asserts it through the shipping server.
|
|
|
|
## Reading it
|
|
|
|
```bash
|
|
curl -sS -H "Authorization: Bearer $TIMMY_METRICS_TOKEN" http://127.0.0.1:4173/api/metrics | jq .
|
|
```
|
|
|
|
For a quick look that does not need `jq`, the report renders the same snapshot as three labelled blocks:
|
|
|
|
```bash
|
|
TIMMY_METRICS_TOKEN=... node scripts/model_metrics_report.mjs --url http://127.0.0.1:4173
|
|
```
|
|
|
|
Exit codes are the point of the command as much as the numbers: `0` means a snapshot was rendered,
|
|
`1` means the server answered non-200 (404 says the route is disabled because no token is configured,
|
|
403 says the supplied token was rejected), `2` means the command itself was called wrongly or the
|
|
server could not be reached. The token is read from the environment or `--token` and is never echoed
|
|
in the report or in an error line.
|
|
|
|
What the numbers mean to an operator:
|
|
|
|
| Signal | Reading |
|
|
|---|---|
|
|
| `readiness.phase: offline` | The worker is not answering. Manual logging is unaffected; do not restart the app for this. |
|
|
| `readiness.phase: loading` | The worker answered but the pinned model is not resident. AI is withheld rather than offered and failed. |
|
|
| `queue.atCapacity: true` | Every model slot is in use. Watch `latency.maxMs`; a growing figure with no completions is a stuck inference, not load. |
|
|
| `failureClasses.provider_unreachable` climbing | Network or worker outage, not caller error. |
|
|
| `failureClasses.schema_invalid` climbing | The worker is answering off-contract. Treat as a regression in the pinned model or prompt, not a user problem. |
|
|
| `app.refusedBeforeModel` climbing with no model traffic | Abuse or a broken client; the model never saw it. |
|
|
|
|
## Known gap
|
|
|
|
`queue.inFlight` is the app's own admission depth for `POST /api/analyze`. The full bounded queue
|
|
(concurrency limit enforcement, cancellation, overload fallback) is issue
|
|
[#17](https://forge.alexanderwhitestone.com/git/stackchain/timmy-talking-turd/issues/17); this
|
|
snapshot reports the slot accounting that exists today and does not claim the rest.
|