---
title: "Get Performance Analytics"
method: GET
path: "/v1/admin/performance"
tags: ["admin"]
---

# Get Performance Analytics

`GET /v1/admin/performance`

System-wide pause-latency aggregates, grouped by (language, had_image, eou_backend, stt_provider).

Pause anchor: `actual_pause_ms` (ground truth, anchored on the user's
last-word end, computed post-hoc by `agent/actual_eou.py`) when present;
falls back to `pause_ms` (framework-estimated, anchored on the EOU
decision moment) when not. The `pct_truth_anchored` column shows what
fraction of each bucket's turns are on the truth-anchored value vs the
estimate-anchored fallback — 100% means fully trustworthy comparison.

Bucket decomposition (same as the per-conversation UI in
web/src/lib/timingBreakdown.ts):
  recognition = max(eou_delay_ms, transcription_delay_ms)   ← they overlap
  hook        = on_user_turn_completed_delay_ms
  llm_ttft    = llm_ttft_ms
  tts_ttfb    = tts_ttfb_ms
  residual    = pause_authoritative − (recognition + hook + ttft + ttfb)

Also returns EOU-effectiveness signals — the only way to evaluate whether
a turn-taking change actually helped without trading pause for cutoffs:
  avg_estimation_error_ms  — signed avg(eou_estimation_error_ms)
                             (>0 = waited past real end; <0 = cut off)
  p5/p95 estimation error  — the distribution shape, since the mean alone
                             hides cut-offs hiding under late waits
  cutoff_rate              — fraction of turns flagged
                             `was_premature_jump_in` by the agent's
                             post-hoc tagging pass

`eou_estimation_error_ms` and `was_premature_jump_in` come from
`agent/actual_eou.py`'s ground-truth pass on the settled STT transcript
(last-word timestamp = true end of speech). Older turns lacking the
post-hoc pass simply contribute NULLs to the means; cutoff_rate counts
NULL as not-a-cutoff (conservative).

`eou_backend` is read from `telemetry.eou_effective.backend` (the
resolved per-session value, respects template overrides — added in PR
#598). For sessions predating that field, falls back to the system-wide
`telemetry.audio_config_at_session_start.eou.turn_detector_backend`.
Final default is 'text' (the only historical option).

Aggregation is on-demand over the existing `sessions.transcript` JSONB +
`telemetry` — no new tables, no backfill. If the JSONB scan becomes slow
at scale, materialise into a rollup table; until then this is the
simplest correct thing.

Sessions without a recorded `telemetry.stt_config.language` are excluded
(per the plan — they would group under `unknown` which isn't actionable).

Param `days` is "7" | "30" | "all"; default 7.

## Query parameters

- `days` string

## Response `200`

Successful Response

- unknown

## Other responses

- `422` — Validation Error

---

[API](https://skmtc.dev/roxels/apis/interviewer.md) · [All operations](https://skmtc.dev/roxels/apis/interviewer/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/roxels/interviewer/revisions/4ef48ed226e1/schema)
