Files
timmy-talking-turd/docs/MODEL-METRICS.md
T
timmy 3e2253a052 wip(#19): privacy-safe model metrics in progress — preserved snapshot before nightly release
Local work in progress, not reviewed and not part of any PR. Committed only so the
nightly release can build from a clean main without discarding it.
2026-11-01 12:02:43 +00:00

4.9 KiB

Privacy-safe model health counters

Issue #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

{
  "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

curl -sS -H "Authorization: Bearer $TIMMY_METRICS_TOKEN" http://127.0.0.1:4173/api/metrics | jq .

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; this snapshot reports the slot accounting that exists today and does not claim the rest.