The source of truth is the explicit route table in
dashboard/server/index.js, with SQL and result shaping in
queries.js. Paths below are relative to the dashboard
origin; the local Compose origin is http://localhost:8080.
AUTH_MODE=basic (default) applies Basic Auth globally using BASIC_AUTH_USER and BASIC_AUTH_PASSWORD.
Without both, startup fails unless AUTH_ALLOW_INSECURE=1 explicitly enables local
unauthenticated use. Only probes bypass this gate. Explicit AUTH_MODE=oidc uses organization
login/session cookies; login/callback and origin-checked local logout are protocol exceptions.
Telemetry/config/static routes require a session and never fall back to Basic.
Chat requires analyst/admin permission in OIDC plus existing readonly/activation gates.
The opt-in device-ingestion namespace uses separate machine credentials as described below;
browser cookies never authorize that namespace.
See SSO operations.
GET /api/session returns mode, role, permissions and, for OIDC, current-user name
and expiresAt, without raw subject/IdP tokens. Optional refreshExpiresAt is the
absolute login deadline, present only while renewal is enabled and a valid refresh
cookie matches the current principal; expiresAt remains the short authorization deadline.
/api/session renews credentials within 60 seconds of short-session expiry when a
valid refresh cookie is present and renewal can extend that deadline; other protected
requests renew expired principals.
/api/config includes this session.
Unauthenticated OIDC APIs return JSON 401, error=authentication_required,
loginUrl=/auth/login. Unsafe session requests without the configured Origin or without
required permission return 403. Transient refresh failures return 503,
error=authentication_temporarily_unavailable, with Retry-After: 5. A rotated refresh
response requires a fresh login because this mode does not retain rotating credentials.
GET /auth/login starts OIDC; GET /auth/callback completes it. POST /auth/logout
requires the configured Origin, clears local cookies, revokes any retained refresh
credential and redirects to /. Concurrent or replayed revocations share bounded work;
see SSO operational limits.
Failed revocation or admission exhaustion returns a visible 503 after clearing
cookies. Logout does not end IdP SSO or immediately revoke copied short principal cookies.
Protocol responses are no-store.
GET /api/installation requires installation:read in OIDC mode; Basic/development
retain their existing full-installation access. It uses the shared response/error wrapper,
but is a current snapshot without usage-range/filter semantics. A single per-process
30-second memo deduplicates DB/receipt probes. Unauthorized roles are rejected before
probing; disabled clients are not queried or shown.
The response has checkedAt, state=operational|needs_attention, release,
authentication, database, schema, clients, staleAfterMinutes and explicit limits.
Each enabled client's seven-day receipt is receiving, stale, empty or unavailable.
Empty is not a token/cost zero; unavailable is not empty. Readonly/schema fields include
their cached probe timestamps. Ledger records do not prove actual DDL/rebuild completion;
receipt does not prove complete telemetry, OIDC configuration does not prove MFA, and no
backup or full commercial-release claim is made. release.revision is null unless the
runtime supplies a valid Git SHA through APP_REVISION.
/api/collector management routes require OIDC collector:manage (analyst/admin).
Unsafe requests retain the existing exact-Origin gate. Responses and downloads are
no-store; viewer/demo requests are denied before store or file access.
| Method and path | Contract |
|---|---|
GET /api/collector |
Feature state, public ingestion endpoint, installScriptUrl, browserEnrollment (helper and package manifest available), Collector version, supported platforms and 30-day token lifetime. |
GET /api/collector/enrollments/:handle |
Read-only view of a terminal's request: {name, platform, code, status, createdAt, expiresAt, userId}. status is pending, approved (by this account), denied, expired or unavailable (decided by another account). userId is the caller's verified email or null; never the polling credential. 404 for unknown handles. |
POST /api/collector/enrollments/:handle/approve |
Same-origin, empty body. Creates the one device under the caller and returns {status, device, userId}; repeating returns the same device. 409 enrollment_denied/enrollment_unavailable, 410 enrollment_expired. 30 decisions per account per hour per replica. |
POST /api/collector/enrollments/:handle/deny |
Same-origin, empty body. Terminal; {status: "denied"}, idempotent. 409 when already approved, 410 when expired. |
GET /api/collector/devices |
Owner-scoped devices and nextCursor; up to 50 records per page. Supply the returned cursor unchanged. |
POST /api/collector/devices |
{name, platform} where name is 1–64 characters without controls and platform is linux or darwin; 201 returns device and the token once. |
DELETE /api/collector/devices/:id |
Revoke an owned device; 204 for existing/already revoked, 404 for absent. |
GET /api/collector/package/:platform |
Authenticated installer archive for linux or darwin; no credentials embedded. |
GET /api/ingest/install.sh |
Public, no-store POSIX sh bootstrap for curl -fsSL URL | sh; embeds only the ingestion endpoint and the enroll.py SHA-256. GET/HEAD only, no query. By default it runs browser enrollment; sh -s -- --token prompts for the attribution ID and device token instead, which is also the only mode without a helper. --user-id ID sets attribution in the token flow; browser enrollment uses the approving login's verified email and applies --user-id only when it has none. |
GET /api/ingest/enroll.py |
Public enrollment helper pinned by install.sh; 404 when browser enrollment is unavailable. |
POST /api/ingest/enroll/start |
Public. JSON {secretHash, name, platform} (SHA-256 hex of the device's own 43-character base64url secret; at most 1 KiB). 201 {verificationUrl, code, pollToken, interval: 5, expiresAt} with a ten-minute deadline. Per replica: 200 per client address and 1,200 in total per ten minutes (429 enroll_rate_limited). |
POST /api/ingest/enroll/poll |
Authorization: Bearer <pollToken>, empty body; 30 per request per minute (429 poll_rate_limited). 200 {status: "pending", interval} or {status: "approved", owner, id, userId, expiresAt, packageSha256}; the device builds ccd1.<owner>.<id>.<secret> locally. 403 enrollment_denied, 410 enrollment_expired/device_revoked, 401 invalid_enrollment. Approved results repeat until the deadline. |
GET /api/ingest/package/:platform |
Installer archive for linux or darwin with exactly one device bearer token; ten per device per hour (429 package_rate_limited, Retry-After: 3600), charged before the file is read, so a missing archive (503) or HEAD also counts. Token checks share the gateway's eight in-flight slots (429 ingest_busy). Does not update lastSeenAt. |
POST /api/ingest/v1/{logs,metrics,traces} |
Device bearer token only; OTLP JSON/protobuf, at most 2 MiB, no content compression. |
POST /api/ingest/renew |
Device bearer token; JSON content type, empty body; six per device per hour. 200 {renewed: false, expiresAt} while more than seven days remain or for an unused replacement, else {renewed: true, token, expiresAt} with a new 30-day token. Device views add renewedAt and reuseDetectedAt (first use of a superseded token). |
Device fields are id, name, platform, createdAt, expiresAt, revokedAt,
lastSeenAt and renewedAt (last completed renewal); dates are ISO strings or null.
Tokens are ingestion-only, expire 30 days after issuance and may remain cached on another
replica for up to ten seconds after revocation. A replacement token becomes current on its
first use (delivery or renewal request) and the previous token is then rejected, subject to
the same cache window. Renewing again before that replaces the unused replacement. Last
receipt does not prove database persistence. These APIs do not use usage range/filter
semantics. See device operations
for installation, identity limitations, resource bounds and offboarding.
Wrapped GET data routes validate these parameters before querying or caching:
| Parameter | Contract |
|---|---|
from, to |
Date strings; use ISO 8601 with timezone. Defaults: to=now, from=to-DEFAULT_RANGE_DAYS. Require from < to; default range is two days and default cap is 90 days. |
intervalHours |
Default 24. Must be finite and in (0,744], even on routes that ignore buckets. Values below one are clamped to one beyond four hours, except the bounded shared-overview fine resolution below. Only rows marked B below and shared overview honor this parameter. |
group |
Exact inferred channel match, normally bedrock, enterprise or unknown; it is not a coding-client selector. |
user |
Case-insensitive substring of the query's identity expression, usually UserEmail. |
model |
Case-insensitive substring after the query's model normalization; scope varies by endpoint. |
project |
Exact project tag, only for the four Usage routes listed below and only while schema.projectColumns === true. (untagged) maps to the stored empty string. |
email |
Supply an exact identity for the two user drilldown routes (/api/users/daily, /api/users/decisions-by-tool). Omission is coerced to an empty string, not rejected as a missing required parameter; callers must not rely on uniform behavior without it. |
includeUnknown |
Only /api/cost/by-user-model recognizes the literal 1, including unknown-channel rows in that result. |
Minute/day widths are rounded by bucket() where needed. Dates are bound to ClickHouse at
second precision. Historical rollup ends, latest-hour data and existence queries have
specific approximations; see time boundaries.
Config, health and chat routes do not use this range wrapper.
Wrapped data responses negotiate gzip for JSON bodies of at least 4 KiB. They retain
Cache-Control: no-store and add Vary: Accept-Encoding; compression does not change
authentication, range/filter cache keys, null values, or JSON escaping. Health
responses and streaming chat keep their separate response handling.
Activation controls /api/config's
enabledClients/codexEndpoint; disabled Claude routes return 404, stop warming/chat.
GET /api/clients/overview returns clients, totals, by_client, by_model,
by_user, by_project, timeseries, tools and metadata below.
GET /api/clients/activity returns daily timeseries rows containing client, t
and sessions, plus clients, bucket_hours=24, effective_range and applied_filters.
It requires userExact; intervalHours must be omitted or 24. It uses the same
authentication, range limits and client/backend/user/model/team filters as overview.
The query aggregates in ClickHouse before returning rows, so a 90-day user heatmap
does not consume overview's 50,000-row detail budget. Claude counts sessions with
token/cost counter increments or supported operational logs, deduplicating the
session across both sources with the existing baselines and historical end alignment:
the counter and log reads run as separate identity-narrowed queries returning distinct
day/session pairs, and the server unions them per day.
Codex counts sessions with supported operational or completion events;
setup metadata and stream deltas only supply filter evidence. Counts are distinct
within each client and UTC day, not across clients or the entire period.
client=all|claude|codex selects enabled sources;
backend=all|bedrock-mantle|bedrock-runtime|anthropic|unknown. Invalid/disabled clients,
arrays and nonempty group/project filters fail before caching.
An exact team filter is accepted with the session semantics in Filter scope,
applied to Claude counters, Claude and Codex logs and the Codex cost facts;
/api/codex/insights rejects it.
Backend is resolved from the model id prefix (region prefix → runtime, bare vendor
namespace → mantle), falling back to the resource tag (Codex); otherwise unknown. Claude
rows are anthropic first for an enterprise session or a bare claude-* record that
carried organization.id; see Claude channels
and ADR-017. Task traces from device
collectors lack the span attribute, so their login calls stay unknown.
Claude model terms normalize. Modeled rows match directly.
Model-less rows use same-client model evidence (logs; Claude also uses token/cost
counters) within the range and identical session (nonempty), user, backend and project.
Coarse session attribution can match several models; rows retain model="".
observed_tokens sums known input/output pairs; tokens_partial discloses incomplete
usage/validation or unsafe sums. Cache and reasoning subsets are not added again.
Unpriced models can still have complete observed tokens. All-unknown is null and
known zero is retained. Primary token displays/CSV use observed counts with coverage
status; canonical tokens and components retain strict nulls for ratios and existing
consumers. These fields also appear in Codex detail summary/Effort rows. See
ADR-014.
by_client rows also carry observed_input: { input_tokens, cache_read_tokens, cache_write_tokens, partial }, the known non-cached input, cache read and cache write sums
from usage rows whose three parts are all known. partial is true when a usage row lacked a
part or a Codex request had no usage-bearing completion (for example an interrupted stream);
parts are null when none is known or a sum is unsafe. The input/cache composition card shows
this labelled subtotal when the strict components are null.
cost_usd uses cost_basis=client_reported (Claude) or aws_list_estimate (Codex), or
(Claude, this endpoint only) computed_estimate/mixed for a token-priced fallback when
no report is usable (ADR-017); cost_estimated
counts those rows. Claude rows for non-Anthropic models carry aws_list_estimate: their
counter cost is rescaled to the AWS list price (ADR-017 amendment),
on this endpoint and in the legacy cost views' reported_cost.
It sums usable reports/estimates even when other records are unpriced.
cost_partial discloses exclusions or an unsafe aggregate; unpriced counts remain.
Groups with only unpriced evidence remain null; explicit zero stays zero.
Positive Claude reports remain usable with missing token fields; zero reports still
require known zero usage. Codex estimates still require valid usage and a known rate
(its own table, or the Claude table for an Anthropic model; price_source: "claude_table").
Table rates come from the merged price catalog: operator overrides, then the AWS Price List
layer, then code tables (ADR-017 amendment).
Cost unit values use the known subtotal and observed denominators, with partial
labels in the UI/CSV. See ADR-013.
User counts union IDs; sessions include client; model identity counts overlap.
by_project uses Codex project.name, not billing projects. Request/TTFT durations
are observed means or null. requests includes api_error attempts.
observed_records: deduplicated logs plus active Claude usage rows. Zero can coexist
with idle counter observations in timeseries; check both for an empty response.
Idle timeline rows carry timeline_observed=true, independent token/cost availability and partial flags
without adding active users/sessions or changing other aggregates. See
ADR-015 for chart zero semantics.
rejected_requests counts explicit HTTP rejections. Identified Codex scopes containing only
rejections have zero recorded completion usage and retain request/error counts.
request_rejections_only labels those shared groups; chart tooltips disclose the
rejected count. Accepted/uncertain requests without usage and unpriced completions
remain unavailable. See ADR-016.
quality.missing_usage counts Codex session/user/model/backend/project scopes without
usage across the range, making affected canonical token folds null and adding to
unpriced. Known cost and observed-token subtotals remain visible with coverage status.
Unpriced counts can combine usage-record counts and unmatched usage-scope counts;
they are not a request-coverage percentage. This cannot detect every dropped response.
Folds share rows; full-usage validity and input/output-pair validity are separate SQL
grouping dimensions, preserving usable costs and observed token pairs. Breakdowns
sort by observed tokens. Active rows and compact idle observations each have a
50,000-row guard (overflow returns 400); idle rows do not consume active-row capacity.
Claude counter/baseline rules remain.
effective_range={from,to,requested_to} applies Claude's resolved end to both clients
when selected; the UI discloses trimming. Native buckets are one minute through four hours,
otherwise one hour. On this endpoint only, an explicit intervalHours between ten minutes
and one hour (exclusive) enables ten-minute native raw counter buckets through one day.
Finer requests over four hours and subhour requests over one day fall back to hourly rollups;
the three-day counter baseline, row limits and query budgets remain. Fine historical queries
keep their requested end instead of flooring it to the hour.
A coarser intervalHours is honored, rounded up to a whole multiple of the
native bucket, and reported as bucket_hours; finer values fall back to native. Codex buckets in
SQL at the native bucket; both clients' rows are then regrouped, so distinct sessions stay exact,
coverage identity stays at the native bucket (scope_t), and bucket-only evidence merges as within
one bucket. Sizes use Claude's resolved end; eligibility for one-day raw resolution uses
the original requested span. Cache keys include display/native bucket sizes and resolved end.
Optional userExact and modelExact select exact row identities in addition to existing
partial user/model filters. Claude model IDs are normalized; Codex routing prefixes remain.
These validated, bound parameters have separate cache keys. /api/codex/insights rejects modelExact
and rejects userExact unless profile=tools, which then compares the identity by equality
(UserEmail, or user.email falling back to enduser.id on the raw-log path) beside any substring user filter.
Other profiles, including the default, return 400 unsupported filter.
Exact responses echo applied_filters={userExact,modelExact,backend}; the UI verifies this
acknowledgment before presenting identity detail, including during a rolling upgrade.
Raw Claude counter inputs also narrow user/model partitions throughout the baseline.
Session channel/backend inference and model-less coverage evidence retain their existing scope.
Bulk delta scope evidence keeps extracted scope dimensions and a fixed delta marker, without
retaining or sorting arbitrary attribute maps; completion deduplication keeps the original
full identity. Token-like attributes on a delta never become completion usage.
Optional view=cost selects the bounded cost profile on this endpoint. Any other supplied
value, including an empty value or array, returns 400 before caching. Its cache identity
is separate from the default view. Cost, token coverage, populations, rejected-request
evidence, effective bounds and exact-filter acknowledgments retain the same semantics.
The response echoes view="cost". It omits by_project, tools, and row fields api_errors, tool_calls,
tool_errors, request_duration_ms, ttft_ms; omitted diagnostics are not measured zeros.
It adds by_user_model, grouped by client, raw user ("" when absent), model,
backend and, for Claude, the inferred session channel (bedrock/enterprise/unknown),
using the same bounded records and cost accumulator as by_user.
These range-level rows retain cost basis, known subtotals, token coverage, and
per-group sessions. Model-level sessions must not be summed into user sessions;
use by_user for user totals. Like by_model_time, it keeps a group only when usage or a
Codex missing-usage scope backs it, so operational-only groups never appear as a known $0
and a by_user user may have no by_user_model rows. This breakdown is absent from the
default profile.
SQL still retains operational records needed for scope coverage, but skips duration/error
calculations and tool-name grouping. Observed requests remains available, including in
the Trends period table and CSV; unsupported counts retain null. Shared cost/executive/trend views, trend baselines and
cost drilldowns use this profile. The default profile retains diagnostic fields.
See cost query performance.
Codex detail log summaries use one query for full-identity deduplication, per-response
pricing and scope evidence. The 50,000-row transfer budget applies to aggregate
dimensions, not raw events. Families are counted before transfer; if their combined
size exceeds the budget, oversized families return only a limit marker under an
equal allowance. Whole-window usage totals are independent of Effort rows. coverage.logs.limited_sections names capped families
(event, usage, effort, scope, operations, latency, tool, approval, runtime) and
sets partial=true. Only affected results are withheld: limited scope evidence keeps
known subtotals partial while withholding strict ratios and session units. See
Codex bounds.
Codex insights additionally returns two log-derived objects in both profiles:
completion_usage, withbasis="recorded_completions"andstatus="observed"|"empty"|"limited"|"unavailable", describes only deduplicated usage-bearing completion logs. Fields includecompletions, canonicaltokens,tokens_partial,cache_hit_rate,cache_write_share,reasoning_share,tokens_per_completion,cost_usd,unpriced,cost_partial, andcost_per_completion. Costs retaincost_basis="aws_list_estimate".identified_sessions,identified_completions,unidentified_completions,identified_cost_usd,identified_unpriced, andidentified_cost_partialdescribe the subset with conversation IDs.cost_per_identified_sessiondivides that subset's known cost by its distinct sessions. Anonymous costs never enter this session unit. Unknown components/prices, zero denominators and limited usage remain unavailable or partial; this cohort is not a replacement for whole-window completeness.completenesscontains booleanmissing_usage,missing_session,usage_limited,scope_limited,operations_limited, andincomplete_tokensreasons.request_countandsession_countare observed whole-window denominators, null when their aggregate family is limited.
Existing summary and Effort fields are unchanged. Both objects use the same
log query and filters; they add no queries or identifiers to the response.
profile=effort on /api/codex/insights selects one log query instead of the default
log, four metric and trace queries. It preserves Effort/usage totals, denominators,
full event identity and missing-usage scope evidence. It skips latency/tool/approval/runtime
breakdowns and quantile work, omits those arrays and summary.tool_success_rate, and
reports coverage.logs.omitted_sections. Unqueried metric/trace coverage has
status="not_requested", records=null, last_seen=null; empty signal arrays do not
assert absent collection. The response echoes profile="effort". Invalid profile
values fail before caching; the profile has a distinct cache identity. The default
profile remains full. See Effort profile.
GET /api/clients/claude-effort supplies the common cost page's independent
Claude effort card. It accepts the same range, user, model and backend filters;
only client=claude|all is valid. Group/project and exact drilldown filters are
unsupported. Authentication, disabled-client checks, validation and cache
isolation use the shared route wrapper.
The response includes range, cost_scope="reported_or_rescaled_counters" and
effort rows with effort, backend, nullable cost_usd, cost_partial,
cost_basis and unpriced. Empty effort becomes unknown; backend identities
remain separate. unpriced counts aggregated user/session/model scopes, not requests.
cost_basis describes the known subtotal's contributing scopes; when all costs
are unknown, it describes their source bases. More than 50,000 scopes returns
400 and asks for a narrower range, without returning a truncated subtotal.
Known counter reports and ADR-017 rescaled estimates retain cumulative/delta
boundaries. Token-derived overview fallback estimates are outside this card's
scope, so its total need not equal the overview total. All-unknown cost is null;
known zero requires observed zero-cost evidence. Filtered responses are cached
independently; this endpoint is not warmed.
Cost pages keep model, effort and input-cache headings visible during loading,
empty results and errors. Model sectors retain version/backend identities and
family colors. Input-cache composition requires complete canonical components;
an observed token subtotal cannot replace a missing cache denominator. The
Codex effort card and detail table share one profile=effort response using the
overview's effective bounds.
GET /api/clients/productivity returns shared Productivity rows for the selected enabled
clients, in one shape:
{clients, by_client: [row], effective_range: {from, to, requested_to}}. It accepts the
overview's client, user, userExact, model, modelExact, backend and team
parameters with the same validation; group/project, invalid backends and disabled clients
return 400 before caching or database work. The effective end is the overview's default
(Claude historical ends beyond four hours floor to the hour); the SPA passes the
overview's effective bounds, which resolve unchanged. It is cached per filter set and
not warmed. Users, sessions, tokens, cost and cost basis are not repeated: the page joins
each row with the overview's by_client row for the same client.
Each row has client, prompts, tool_calls, tool_successes, tool_failures,
tool_unknown, tool_success_rate, decisions_total, decisions_allowed,
decisions_denied, decisions_unknown, permission_allow_rate, tool_calls_per_prompt,
ttft_samples, ttft_p50_ms, ttft_p90_ms, ttft_source, permission_wait_samples,
permission_wait_p50_ms, by_tool_decisions, by_tool_results, top_users,
activity_index_users, activity_index_scored, active_user_seconds, active_cli_seconds,
commits, prs, loc_added, loc_removed and unavailable.
| Field | Claude Code | Codex |
|---|---|---|
prompts |
user_prompt log events |
codex.user_prompt log events (one per user turn) |
| Tool outcomes | tool_result: success is success=true without error; failure is success=false or an error |
codex.tool_result success=true/false |
| Decisions | tool_decision accept/reject; source from source |
codex.tool_decision approved/denied families and source, as in /api/codex/insights |
ttft_p50_ms, ttft_p90_ms |
Beta llm_request span ttft_ms (per model request) |
codex.turn_ttft duration_ms (per turn) |
permission_wait_p50_ms |
Beta tool.blocked_on_user span duration_ms (the permissionWaitOverhead source, v2.1.214+) |
null, not_aggregated |
top_users |
Activity index over userLeaderboard inputs |
null, not_derived |
| Active time, commits, PRs, LOC | Counter increases (incFlat), null without in-window counter evidence |
null; see unavailable |
Totals and per-tool rows come from one log scan per client: each total is the sum of its
kind's rows. by_tool_results and by_tool_decisions list, per client and kind, the ten
tools with the most records (ties by name), then at most one remainder row with
other: true, tool: null and tools (the number of folded tools); its values are
computed over the folded records, so its p50 is exact. A missing tool name is "".
Result rows have tool, other, tools, calls, successes, failures, unknown,
success_rate, duration_samples and duration_p50_ms (valid duration_ms only).
Decision rows have tool, other, tools, decisions, allowed, denied, unknown,
allow_rate, source (the most frequent source, ties by name; "" when missing) and
source_decisions. An empty list is a measured absence; a missing response is null.
Permission decisions include automatic approvals and are not code acceptance.
top_users lists at most ten Claude users (UserEmail, unmasked like
/api/users/leaderboard; the SPA masks them) with user, productivity_score,
accept_rate, loc_added, commits, sessions, active_days and decisions. Inputs
mirror /api/users/leaderboard (session count, added lines, commits, code-edit decisions
from incFlat; distinct hourly session-count days) and withProductivityScore over the
effective range, but per user across channels and under the shared filters below rather
than the Claude detail grouping. Users rank by score; a user without code-edit decisions
or with a malformed input has a null score and ranks last. activity_index_users counts
the observed users and activity_index_scored those with a score.
Log events are deduplicated by timestamp and both attribute maps; spans by trace and span
id. Rates use known outcomes only (tool_successes / (tool_successes + tool_failures),
decisions_allowed / (decisions_allowed + decisions_denied)); the unknown remainder is
reported separately, and an empty denominator yields null. P50/P90 are exact nearest-rank
lower quantiles of valid nonnegative samples; no samples yields null. A measured zero count
stays 0. unavailable maps a null field to a reason: not_collected (default OTel has no
source; Codex prs, commits, loc_added, loc_removed need Git/CI evidence),
not_aggregated (no confirmed source metric) or not_derived (a dashboard-derived value
whose inputs are missing; Codex active time, because codex.turn.e2e_duration_ms is turn
elapsed time, not human active time, and Codex top_users, because the activity index
needs lines and commits Codex does not emit). Codex permission_wait_p50_ms is
not_aggregated. Claude rows have an empty map.
Filters: Codex follows the overview's event-level rules (user identity, model with
same-session evidence for model-less events, backend from model/resource tag, session
team). Claude events, spans and counters carry no usable model, so model, modelExact
and backend select sessions whose token/cost counters match in the range plus the
three-day lookback (the overview's counter backend rule); user/userExact match the
counter UserEmail or the log/span identity; team is the session team. The same rules
apply to the per-tool rows, latency and top_users (whose active days use the same session
scope on the hourly rollup). All Claude channels, including unknown, are included. These
are activity observations, not productivity, code quality or saved time.
modelTime=1 is accepted only by /api/clients/overview; any other value returns 400
before caching. It is part of the cache key; the shared default cost profile warms
view=cost&modelTime=1&timeFormat=compact. Custom windows, filters, resolutions and
legacy time-row responses may remain cold;
/api/codex/insights ignores it. It adds by_model_time: one row per client, t, model, backend from the
same fold and buckets as timeseries, with Codex raw model IDs (routing prefixes
kept; Claude models are normalized as in by_model) and the usual cost/token/coverage fields. A row exists only where Claude counter
usage, a Codex usage-bearing completion or a Codex missing-usage scope backs it;
operational-only records (requests, tools, TTFT, stream errors, rejected-only requests)
attach to such a row but never create one, so their buckets appear only in timeseries.
Each row's unpriced_reasons counts Codex unknown_backend, scope, unknown_model,
invalid_usage, or Claude report_missing, report_zero_with_tokens, plus
missing_usage (usage scopes without usage; model-less scopes stay under model=""); the
counts sum to unpriced. Claude report_missing also covers an invalid (negative or
non-numeric) report and a zero report on a counter row whose cost_observed is 0;
report_zero_with_tokens also covers a zero report beside unknown tokens. Per client,
t, known cost_usd sums to the timeseries row; idle Claude observations stay in
timeseries only. Codex estimates are never recomputed from aggregated tokens. Each
unpriced Codex response has one unpriced_reason, checked in order: backend, scope
(us-gov., a commercial routing prefix on mantle, or no rate for the scope/tier), model rate,
then usage validity; a configured rate with a non-finite estimate reports unknown_model.
It also adds by_user_time: the same backed usage regrouped per client, t, user
(raw identity, "" when absent) with the same fields except model (backend is the rows' single backend or mixed), plus
unpriced_reasons; per client, t its known cost_usd and unpriced sum to the
by_model_time rows. Each client keeps its top 20 users with backed usage (known range
cost_usd, then observed_tokens, then identity) and ""; the rest fold into one user: null
row per client, t whose rest_users is the client's folded user count (null on named
rows), sorted last.
Optional timeFormat=compact requires view=cost&modelTime=1; other supplied values
or combinations return 400 before cache lookup or database work. It has a separate
cache identity and echoes time_format="compact-v1". Only by_model_time and
by_user_time change representation to {columns, dictionaries, rows}. Each row
is an array in columns order. Columns present in dictionaries use zero-based
indices into their dictionary; null remains null. Other cells retain their original
values. No fields, identities, buckets or coverage information are removed.
The format changes only the two time tables; totals, rankings and timeseries
retain their representation. Filters and cost calculation are unchanged.
User rankings sort descending by observed tokens, then client and raw user identity
for ties, so identical usage does not reorder users during refresh.
The SPA requests this format and reads rows lazily; older array responses remain
supported. Malformed or unknown compact formats are unavailable, never zero.
This reduces response and cache size; it does not reduce the underlying SQL scan
or prove a fleet capacity limit.
Skill invocations per selected enabled client (client=all|claude|codex) with the shared
filters (user/userExact, model, backend, team; Codex model filtering uses the overview's
session model evidence). Claude reads skill_activated logs, Codex codex.skill_invocation
logs; exact duplicate deliveries (same Timestamp and attribute maps) count once. The window
ends at the overview's effective end, so the page joins it to the overview's bounds.
Response {clients, skill_limit, by_client, effective_range}. Each by_client row is
{client, skills, skills_limited, totals}: skills are {skill, calls, explicit, automatic, nested, users, sessions}, most called first, at most skill_limit (200; skills_limited past
that). explicit is Claude
user-slash or Codex explicit, automatic Claude claude-proactive or Codex implicit,
nested Claude nested-skill (null for Codex, which has no such kind); other triggers count
only in calls. Events without a label-shaped name stay in one unnamed row (skill null;
its users/sessions null when several raw names fold into it). totals sums the returned
rows ({skills, calls, explicit, automatic, nested}; skills counts named rows; a malformed
count makes its total null). skills is null for a malformed read, shown as not collected.
The cache key carries the client, backend, exact filters and effective end. An empty list
means no skill event was recorded, not that no skill was used.
Returns the token economics cost chain for the selected enabled
clients. It accepts the overview's client, user, userExact, model, modelExact,
backend, team and intervalHours parameters with the same validation; group/project,
invalid backends and disabled clients return 400 before caching or database work. Only
enabled clients are queried. It is cached per filter set, window and bucket, and not warmed.
{ effective_range: {from, to}, previous_range: {from, to}, bucket_hours,
by_client: [{ client, cost_basis, current, previous, decomposition,
unavailable: {chain, decomposition, outcome},
coverage, previous_coverage, trend, models, outcome, previous_outcome, exemplars }],
combined: { cost, previous_cost, decomposition, unavailable_clients } }
effective_range.from floors from to the hour; to floors the overview's effective end to the
hour. previous_range is the window of the same length ending at effective_range.from.
bucket_hours is the trend bucket: the overview's bucket for the range and intervalHours,
rounded up to whole hours and at least one hour.
| Field | Content |
|---|---|
current, previous |
{users, sessions, turns, requests, tokens, cost, factors, cost_per_million_tokens, unavailable: null}; factors has users, sessions_per_user, turns_per_session, requests_per_turn, tokens_per_request, cost_per_token; a factor whose denominator is zero is null (undefined), not 0, while the totals stay. Null when that window's chain is unavailable or the window is gated. |
decomposition |
{contributions, bundles, total_change, unavailable}; contributions per factor, bundles growth/waste/price. total_change is current.cost - previous.cost, null when either chain is missing. unavailable is the current chain's reason when it is missing, else the previous window's gate, no_previous or zero_factor. |
unavailable |
Reason or null for chain (current window), decomposition and outcome. |
coverage, previous_coverage |
{sessions, matched_sessions, excluded: {no_turns, no_requests, cost_incomplete, tokens_invalid}, cost_known, cost_matched, cost_unattributed}. tokens_invalid covers sessions with an unusable token subtotal or with requests but zero canonical tokens. cost_known includes cost_unattributed, the known cost of usage rows without a session. |
trend |
Current-window buckets {t, cost_per_user, cost_per_session, cost_per_1k_requests, tokens_per_request, cache_hit_rate}; null values where the bucket's chain is unavailable (no matched session or no identified user). |
models |
{model, cost, cost_partial, requests, request_share, tokens, cost_per_million_tokens, small} over matched sessions, by cost. A model seen only in request rows, with no counter usage row in any matched session, has cost, tokens and cost_per_million_tokens null (unavailable, not zero); cost_partial marks a known subtotal when some of its sessions lack usage rows. |
outcome, previous_outcome |
Claude: {commits, prs, loc_added, cost_per_commit, cost_per_pr, cost_per_kloc} over matched sessions; a count and its ratio are null without in-window counter evidence (ADR-018). Codex: null (unavailable.outcome = "not_collected"). |
exemplars |
{eligible_users, levers: [{lever, direction, median, top: [{user, value, sessions, turns, commits}]}]}; top holds up to 10 eligible users in lever order and median covers every eligible user; commits is null for Codex and for users without commit evidence. Identities are unmasked like other per-user responses; the SPA masks them. |
combined |
cost/previous_cost sum the per-client matched costs, null if any client's chain is missing; decomposition sums per-factor contributions and is unavailable (first blocking reason) if any client's decomposition is. unavailable_clients lists each blocking {client, reason} in by_client order, empty when the decomposition is available. |
Reasons: window_too_short (effective window under one hour), not_backfilled
(SESSION_TURNS_READ_FROM unset or after the window's start), retention (the window's start
older than 90-day log retention), no_matched_sessions, no_users, no_previous (previous
chain missing), zero_factor (a cost or factor is zero in either window) and not_collected
(Codex outcomes). Each window is gated before any query. window_too_short, or
not_backfilled/retention for effective_range.from, makes every section null and runs no
query. not_backfilled/retention for previous_range.from skips only the previous-window
queries: previous, previous_coverage and previous_outcome are null, decomposition and
unavailable.decomposition carry the reason, unavailable.chain stays null, and the combined
previous_cost is null.
Filters: team passes to the usage sources and user/userExact to the turns source (log
identity). Codex usage also applies user/userExact (the same log identity); Claude usage is
read without them and kept for sessions whose turns matched, so counters without UserEmail
(Bedrock) stay counted. model, modelExact and backend select whole sessions with
at least one matching usage row (Claude normalized model, Codex raw model; backend equality).
With a team, model or backend filter, sessions absent from the filtered usage rows are dropped
from the turns as well, so they never enter coverage. Outcome counters are read per session and
kept only for matched sessions.
Forwarded parameters are not universally implemented. filterCond applies a filter only when the query supplies its column expression.
| Queries | Actual scope |
|---|---|
| Most A/B queries | Exclude unknown by default; an explicit group=unknown bypasses that default exclusion. |
kpiSummary, costSummary, activeUsers, adoptionLevels, adoptionTimeseries |
Include unknown-channel rows when no channel is selected. |
activeUsers, adoption routes |
Honor group/user; ignore model and project. activeUsers honors team; adoption routes reject it. Adoption snapshots ignore from and use windows ending at to. |
| Integrity version cohorts | Honor group and team; ignore user/model/project. |
| Plugin inventory | Range and team only; no group/user/model/project filtering. |
| Subagent fanout, skill activations, compaction, refusals, exhausted retries, permission wait, interaction breakdown | Honor group/user; ignore model/project. TTFT separately supports row-level model filtering. |
| Activity density, tool outcomes, permission occupancy | Honor group/user and session-level model filtering; ignore project. Unknown channels are excluded by default. |
| Session turns | Honor group/user. A model filter selects sessions that used a matching model (rollup lookback semi-join) and keeps each selected session's whole cost and turn count. Ignore project. Unknown channels are excluded by default. |
| Mixed metric queries | Model-bearing rows match directly; model-less session/commit/PR/LOC/decision/activity rows use a matching-session semi-join when modelMixed is configured. |
| Many log queries | modelViaSession accepts every matching session's events if that session has a matching model in the rollup lookback. This is not event-level model attribution. |
| Reported-vs-computed and entrypoints | Match the model on each api_request log row. |
| User daily/decisions/heatmap | Exact email and optional group; ignore global user/model/project. With no group, include all channels. Heatmap uses 91 days ending at to, ignoring from. Reject team. |
| Projects, permission modes, decision sources, entrypoints | The only queries applying project filters. Projects filter identity using the email/EndUserId fallback; the other three filter UserEmail. |
| Team (all applying routes) | Session semi-join (teamViaSession, else the declared modelViaSession/modelMixed session), not event-level attribution. Each session's team is its most frequent otel_logs value of resource, else log, attribute team (ties lexically first) over the range plus the three-day counter lookback. Rows without a session, and sessions without team evidence, never match. |
GROUP_MODE=single affects frontend presentation only. The project filter is silently dropped
on both server and frontend unless the project probe is true. /api/usage/projects also
returns [] while gated off; the other three routes can run without the new columns when
no project predicate is applied. The probe checks logs only, not every table or migration.
team is a client-provided OTel resource attribute, not verified identity (like user.email).
The parameter is an exact match after trimming; empty means no filter. A non-string or a
value longer than 256 characters returns 400 invalid filter before caching. It is part of
the result cache keys, and team-filtered requests never mark a legacy route for warming.
The adoption routes, the two email drilldowns, the task routes, /api/codex/insights and the two team
lookups return 400 unsupported filter with detail the team filter does not apply to this endpoint. The SPA does not forward team to them; the filter bar discloses the views it
does not cover.
All endpoint tables below describe GET JSON responses. Unless explicitly marked object, a response is an array. Numeric ClickHouse fields may arrive as strings; derived JavaScript fields are numbers or null. B means a configurable time bucket, not necessarily a day.
Token fields means input_tokens, output_tokens, cache_read_tokens, cache_write_tokens.
Priced row means those fields plus reported_cost, computed cost, and unpriced.
An unknown server rate yields cost: null, unpriced: true; it does not invalidate a valid
client-reported spend value. Summary/effort/agent aggregates retain unpriced_tokens.
Trace result means {unsupported:false, rows:[...]} when matching spans exist, otherwise
{unsupported:true, minVersion, rows:[]}. minVersion is present on the unsupported result,
not guaranteed on success. An empty trace result is not measured zero; missing tables or
query failures still use the normal error path.
| Path | Fields and grain |
|---|---|
/api/overview/kpi |
Per group: sessions, users, commits, prs, total_tokens, input_tokens, output_tokens, lines_of_code; observation flags commits_observed, prs_observed, lines_of_code_observed. Its row-existence user count is not a substitute for active-users. |
/api/overview/active-users |
Object: users, bedrock_users, enterprise_users; distinct nonempty emails with session-counter rows in range. |
/api/overview/tokens-timeseries |
B, per t, group: tokens, input_tokens, output_tokens. |
/api/overview/cache-efficiency |
Per group: cache_read, input_side, cache_read_ratio, uncached_input, cache_write, output_tokens. Input side includes uncached input, cache read and cache creation. |
/api/overview/model-distribution |
Per group, model: tokens, input_tokens, output_tokens. |
/api/overview/session-turns |
Object {summary, users, sessions}, Claude Code only. summary: sessions, sessions_with_turns, turns, median_turns_per_session, p90_turns_per_session, known_cost, cost_partial, cost_per_turn, model_calls_per_turn, sessions_without_turns, sessions_cost_unavailable. users (top 20 by known cost): user, sessions, turns, known_cost, cost_partial, cost_per_turn, cost_per_session. sessions (top 20 by known cost, unavailable last): session, user, group, started (first in-window prompt/model-call event, ISO UTC, or null), turns, model_calls, known_cost, cost_partial, cost_per_turn. Unavailable costs and ratios are null, never 0. Definitions: session turns. |
| Path | Fields and grain |
|---|---|
/api/productivity/normalized |
Per group: loc, tokens, loc_per_million_tokens, commits, commits_per_million_tokens. |
/api/productivity/decisions |
Per group, decision: n, from the code-edit permission-decision counter. |
/api/productivity/decisions-by-tool |
Per group, tool, decision: n; excludes empty tool names. |
/api/productivity/active-time |
B, per t, group: active_seconds, summing active-time types. |
/api/productivity/active-time-summary |
Per group: user_seconds, cli_seconds, user_seconds_observed, cli_seconds_observed, and distinct sessions in the active-time aggregation. |
/api/productivity/activity-density |
Per group: observed_scopes, matched_scopes, nullable matched_commits, user_hours, commits_per_user_hour, and fractional coverage. Scopes are user/session pairs with either relevant counter observed; ratios use only matched scopes. |
/api/productivity/tool-outcomes |
Per group: total, succeeded, failed, unknown, nullable success_rate, unknown_rate. Fractions, no top-tool cap; both unprefixed and claude_code.tool_result events. |
/api/productivity/permission-occupancy |
{unsupported, limited, rows}. Per group: interactions, matched_interactions, ambiguous_interactions, nullable interaction_ms, blocked_ms, blocked_share, and fractional coverage. |
/api/productivity/agenticness |
B, per t, group: prompts, tool_calls, tool_calls_per_prompt, from log events. |
/api/productivity/engagement |
B, per t after filters, with no group column: users, sessions, prs, prs_per_user. |
/api/productivity/loc-timeseries |
B, per t, group: loc_added, loc_removed. |
/api/productivity/languages |
Per group, language: edits, accepted; empty language becomes unknown. These are decision counts, not quality measurements. |
/api/productivity/permission-wait |
Trace result, minVersion: "2.1.214"; per group, app_version: p50_wait_ms, p95_wait_ms, n, from tool.blocked_on_user. |
/api/productivity/ttft |
Trace result, minVersion: null; per group, model: p50_ttft_ms, p95_ttft_ms, n, from llm_request. |
/api/productivity/interaction-breakdown |
Trace result, minVersion: "2.1.214"; per group: interactions, p50_interaction_ms, p95_interaction_ms, llm_share, tool_exec_share, blocked_share. |
Trace shares join child spans to interactions by TraceId. Overlap means shares can sum
above one; they are not a partition of elapsed time. These three trace routes are excluded
from cache warming. The beta setting and matching span/version coverage must be checked
before interpreting absence.
The three activity-density/tool-outcomes/permission-occupancy routes also skip cache
warming and use the authenticated Claude route wrapper. Occupancy's blocked_share
unions overlapping wait intervals within each matched interaction; it is distinct from
the additive share in interaction breakdown. No matched wait produces null, not zero.
unsupported=true means no valid eligible interactions; limited=true with empty rows
means the 20,000-span bound or a conflicting span identity prevented calculation.
Neither state swallows database errors. See measurement and coverage rules.
| Path | Fields and grain |
|---|---|
/api/usage/tool-mcp |
Per group, tool, mcp_server: ok, fail, total from tool_result; ordered by group then volume, 50 returned rows total. Only explicit success/false flags enter ok/fail. |
/api/usage/tool-decisions |
Per group, tool, source: accepts, rejects, n, accept_rate, from tool_decision. Selects the top 20 tool names by unfiltered fleet volume in range, then applies outer filters. Rate is accepts/(accepts+rejects), not accepts/n. |
/api/usage/skills |
Per group, skill: invocations, est_cost_usd. Counts incFlat cost-series rows and sums reported cost; not a direct activation count. |
/api/usage/connectors |
Per group, connector: users, calls, ok, from tool_result with a nonempty MCP server. |
/api/usage/subagent-fanout |
Per group: subagent_completions, interactions, avg_subagents_per_interaction; denominator is distinct nonempty PromptId among completion events. |
/api/usage/skill-activations |
Per group, skill, trigger: invocations, counting skill_activated events. |
/api/usage/compaction |
Per group, trigger: compactions, sessions, compactions_per_session, avg_compression_ratio; counts only compactions with positive pre-token count. |
/api/usage/plugins |
Fleet plugin, marketplace: session_loads, sessions, from nonempty plugin_loaded names. |
/api/usage/tool-latency |
Per group, tool: uses, errors, p50_ms, p95_ms; errors include explicit false success or a nonempty error attribute. Ordered by group then uses, 50 returned rows total. |
/api/usage/commands |
Object {commands, prompts}. Commands: group, command, uses, users, excluding empty command names. Prompts: group, prompts, p50_len, p95_len, over all user-prompt events. |
/api/usage/hook-overhead |
Per group: executions, total_seconds, p95_ms, blocked; blocked counts executions with num_blocking > 0, not the sum of that attribute. |
/api/usage/mcp-health |
Per group, server: attempts, connected, failed, p95_ms from mcp_server_connection. Other statuses also enter attempts. |
/api/usage/projects |
Per group, project: reported cost_usd, tokens, sessions, users; empty project becomes (untagged). Counts sessions/users only with cost/token rows in range. Returns [] unless the project gate is true. |
/api/usage/permission-modes |
Per group, from_mode, to_mode: changes, sessions from permission_mode_changed; mode values pass through. |
/api/usage/decision-sources |
Per group, decision_source, decision_type: tool_results, share from tool_result, excluding empty decision sources. Share is within channel, rounded to three decimals. |
/api/usage/entrypoints |
Per group, entrypoint: requests, sessions, reported cost_usd, users from api_request; empty entrypoint becomes terminal. Reads the entrypoint map directly. |
tool_decision.source and tool_result.decision_source describe different event populations.
MCP health reads server_name, while connector/tool-result queries use promoted fields
parsed from tool_parameters. Counts, quantiles and rates therefore need their own denominators.
| Path | Fields and grain |
|---|---|
/api/reliability/refusals |
Per group: user_visible_refusals, server_hidden_refusals; the latter have server_fallback_hop='true'. Keep them separate when describing visible failures. |
/api/reliability/retries-exhausted |
Per group: exhausted_retries, avg_total_attempts, avg_retry_duration_ms. It does not isolate throttling as the cause. |
/api/reliability/api-errors |
Object {byModel, byStatus}. By model: group, model, requests, errors, total, error_rate. By status: group, status_code, errors; missing status becomes no-http-status. |
/api/reliability/api-latency |
Object {byModel, byEffort}; each row has group, model/effort, requests, p50_ms, p95_ms. Empty effort becomes unknown; duration comes from api_request.duration_ms. |
/api/reliability/reported-vs-computed |
Bare array per group, app_version, model: requests, priced-row fields, ratio = reported_cost / cost when computed cost is positive (otherwise null), plus cost_5m/cost_1h (computed cost at each cache-write TTL, independent of the server's PRICING_CACHE_WRITE_TTL default) and ttl_share = (reported_cost - cost_5m) / (cost_1h - cost_5m) — near 0 means the reported cost prices cache writes at the 5-minute tier, near 1 means the 1-hour tier; null when the denominator is 0 (no cache-write tokens). Claude Code 2.1.300+ also logs cache_creation_5m_tokens/cache_creation_1h_tokens; the rows add cache_write_5m_tokens/cache_write_1h_tokens sums, cache_split_coverage (split share capped at 1 for positive total writes, otherwise null), cost_split (split tokens at their TTL list price, a nonnegative remainder at the server's PRICING_CACHE_WRITE_TTL assumption) and ratio_split = reported_cost / cost_split. Coverage 1 means a zero assumed remainder, not agreement between the total and split fields; 0 means cost_split equals cost. Over-total splits remain priced with a zero remainder, including when total writes are zero and coverage is null. Source is api_request logs. |
/api/integrity/version-cohort-sessions |
Per group, app_version: distinct sessions with nonempty version and session-counter rows in range. |
/api/integrity/version-cohort-cost |
Per group, version_cohort: reported cost_usd, tokens, usd_per_million_tokens. Cohorts are pre-2.1.214 and >=2.1.214; raw series are differenced locally. |
API error rate is api_error / (api_request + api_error) event counts. The code does not
establish whether failed requests also emit api_request, so this is not a deduplicated
request failure probability. Reported/computed ratios diagnose differences in pricing,
TTL assumptions or collection; they do not prove a specific bug or invoice accuracy.
ttl_share isolates the cache-write TTL axis specifically, since the four token types stored
by OTel (input, output, cacheRead, cacheCreation) carry no TTL dimension. It does not
attribute any remaining reported/computed gap to collection completeness or a client pricing
bug — see cost accuracy review.
| Path | Fields and grain |
|---|---|
/api/users/leaderboard |
Per user, group: sessions, tokens, input_tokens, output_tokens, loc, commits, prs, accepted, decisions, active_days, user_active_days, accept_rate, productivity_score (both null without a positive decision denominator). |
/api/users/tools |
Per user, group, tool: uses from tool-result logs. |
/api/users/skills |
Per user, group, skill: invocations, counting raw cost datapoints rather than activation events. |
/api/users/cost-efficiency |
Per user, group: computed cost, unpriced, reported_cost, reported_unpriced, additive known_reported_cost and reported_partial, loc, commits, cost_per_loc, cost_per_commit. Ratios require usable reports from every included model; partial costs do not establish valid ratios. |
/api/users/daily |
Exact-email daily t, sessions, loc, tokens, commits; fixed daily buckets. |
/api/users/decisions-by-tool |
Exact-email group, tool, decision, n. |
user_active_days is the distinct-day union within the query's filtered population, shared
across a user's channel rows; summing per-channel active days can double-count dates.
Both email drilldowns are excluded from cache warming.
/api/users/tasks (not warmed; team rejected) lists at most 20 tasks for one
exact user: required userExact (email, else enduser.id, equality match), optional
client, backend, group (Claude tasks only; 400 unless Claude is selected),
workflowRunId, workflowName and sort=tokens|cost|subagents|duration (default tokens): most observed tokens, highest
known cost, most subagents, slowest. Unknown values (null cost, Codex subagents) sort
after every known value; ties fall back to observed tokens, then the task id. Response
{tasks, unattached_codex, coverage, partial: true}; coverage.<client> is observed,
empty or not_requested (Codex also limited when a row guard is hit). Only
trace-enabled users emit spans, so partial is always true. Claude and Codex tasks are
merged, sorted and capped together. Claude cost is priced after the query (reported cost
joined from logs, else pricing), so sort=cost folds the top 100 Claude interactions by
observed tokens and orders that pool by cost; the response then adds sort_candidates: {claude, claude_limit: 100, limited} (claude: rows considered, limited: the pool was
full, so a costlier task outside the token top 100 may be missing). It is present only for
sort=cost with Claude selected. workflowRunId (wf_ plus 1-128 identifier characters,
else 400) keeps Claude tasks with any span whose workflow.run_id equals it; a workflow's
agents nest under the interaction that started it, so the run usually lists as one task.
workflowName (a label: letter or digit, then up to 127 of letters, digits, _.:-; else
400) keeps tasks with a span whose workflow.name equals it and whose workflow.run_id is
non-empty (a name without a run id is not a workflow run). Both require client=claude
alone (400 otherwise, including the default when Codex is enabled) and match only
trace-enabled clients' spans. Codex turns are all costed before sorting. A Claude task is one
interaction span starting in [from, to); its llm_request spans are read through
to + 24h. Each task has id (claude:<TraceId>), started_at, duration_ms
(duration_basis: "interaction_span"), normalized models, llm_calls, subagents
(distinct non-empty agent_id), tokens (input, cache_read, cache_write,
output from span attributes, observed_tokens, tokens_partial; unknown is null)
and cost_usd/cost_partial/cost_basis. Claude tasks add workflows: one
{run_id, name} per distinct valid run id on any of the trace's spans, sorted by run id
([] when none); name is null when missing or not label-shaped, and the smallest valid
label wins if spans of one run disagree. Only wf_-shaped run ids count, for the filters too. Device collectors drop
names that are not label-shaped before ingest. Per call: Anthropic models use the
api_request cost_usd joined by request id (client_reported), else computeCost()
(computed); non-Anthropic models use the ADR-017 per-request AWS list estimate
(aws_list_estimate, null when unpriced). Calls with different bases make mixed;
cost_basis is null for a task without calls.
A Codex task is one turn (id codex:<conversation.id>:<turn id>) of client=codex spans
whose first span starts in [from, to) and whose turn id is UUID-shaped; spans group by
turn id alone, so a synthetic id that repeats across conversations (auto-compact-N)
cannot identify one turn and is never listed; spans are read in [from - 24h, to + 24h) so a
turn crossing a bucket edge is listed once, by its starting bucket. Turns starting in
the neighbouring 24h are attachment context only: completions they win are neither
listed nor unattached here.
duration_ms is the observed span window (min start .. max end, duration_basis: "observed_span_window"), llm_calls counts the turn's model-call spans (codex.sampling; a turn without one,
from older Codex such as 0.159.3, counts run_sampling_request instead, never both), and subagents is
null (no attribute links a subagent conversation; the detail's agents follows the trace). Spans carry no usage: tokens and cost come only from unique
usage-bearing response.completed completions of the same conversation whose event
time falls in [start, end + 5s] (latest-starting turn when several; each completion
once), priced per completion with the Codex AWS list estimate (cost_basis: "linked_estimate"). tokens.input is the uncached remainder beside cache_read/
cache_write, so observed_tokens is input + output without re-adding cache or
reasoning (ADR-014). Unpriced or
token-less completions leave the cost known-partial (cost_partial) or null. Codex
tasks are never group-filtered; with backend, a turn needs a linked completion on
that backend (a turn without one is unknown). When Codex is selected,
unattached_codex is {completions, cost_usd, cost_partial, turns_without_conversation, turns_without_unique_id}: the user's completions in [from, to) linked to no traced turn
and their known cost (0 when none, null when all unpriced), plus the bucket's turns skipped
for lacking a conversation id and, separately, for a non-UUID turn id (distinct ids: one
synthetic id shared by several conversations counts once). Completions inside a skipped
turn's window are unattached unless another turn wins them; otherwise it is null.
/api/users/tasks/detail (not warmed; team rejected) returns one task of the list:
required id (exactly as listed: claude:<32 lowercase hex> or
codex:<conversation id>:<turn id> with identifier characters and a UUID-shaped turn
id; else 400), optional userExact,
from and to (the list's bucket). A disabled client's id is 400. A task that does not
belong to userExact in that bucket, or does not exist, returns 404 {"error": "not found"}
either way. Without userExact (a shared task link) the owner is resolved: the Claude
interaction's identity, or the single identity on the Codex turn's spans of that
conversation (none or two: 404). Any reader can already pass any exact user, so this adds no
access. Response {task, lanes, spans, truncated, span_total, why, context, environment, owner}
(Codex adds agents and activity), owner being the exact identity the task belongs to:
task: the list row (same fold, same totals) for that task; Codex addscompletions_without_span(linked completions matching no model-call span).lanes:{id, label, kind, spawned_by, llm_calls, cost_usd, cost_partial}. Claude hasmainplus one lane peragent_id;kindisgeneral-purpose,Explore,forkorcustom,labelis the spawning span'ssubagent_type(null when not found) andspawned_byis the drawn tool span that spawned the lane's firstllm_request(thetoolspan when thetool.executionparent is folded into it). Codex has onlymain.spans:{id, parent, lane, kind: llm|tool|wait, name, start_ms, duration_ms, attrs};start_msis the offset from the task start andparentis null when the parent is not in the payload. Claude emits one tool span per tool call: atool.executionspan whose parenttoolspan is present folds into that span (its allowlisted attrs fill keys the tool span lacks;execution_msis its duration, whileduration_msstays the whole call including permission wait), and children of the execution span point at the tool span. An execution span without itstoolspan is returned as the tool span. Codex spans are the turn's spans that carry the task's conversation id or none. LLM spans addtokens,cost_usd,cost_basis; Codex model-call spans (codex.sampling, orrun_sampling_requestfor legacy turns) carry the linked completion's values andcompletion: {model, effort, ttft_ms, duration_ms, count}.attrsholds only allowlisted keys: LLMmodel, token counts,ttft_ms,attempt,error_class,status_code,stop_reason,query_source_safe,effort,speed; tooltool_name,subagent_type,skill_name,anthropic.mcp.server.name,bash_command_class,success,error_class; waitdecision,source; Codexmodel,tool_name,aborted. Current Codex putstool_nameon every tool-call span (collaboration tools included), so a Codex tool span without it is treated as one of Codex's own operations (turn scaffolding,mcp.runtime.*,codex.tool_blocking) and carriesinternal: true. This is a heuristic tied to that emission; the UI hides those spans by default and says how many.truncated/span_total: at most the newest 5000 spans. Truncated Claude details null whole-task and lane totals andtask.models(the newest spans' models are not the task's) and return nowhy; Codex totals do not depend on spans.why:{rule, value}forcache_read_share(share of complete canonical tokens),context_growth({first, last, calls}main-lane input context),top_lane_share({lane, share}, two or more lanes, all costs known),most_expensive_call({span, cost_usd}, all costs known),retries(calls withattempt > 1; omitted unless every call carriesattempt) andpermission_wait(Claude wait-span milliseconds). A rule whose inputs are unavailable is omitted, never zero-filled. Data rules: data reference.environment: what the Claude task's session carried, from itsmcp_server_connectionandplugin_loadedevents between 14 days before the task start and its end (lookback_days):mcp{connected, failed, other, servers: [{name, status}]}by each server's latest status (name/statusnull unless label-shaped) andplugins{count, skills, agents, commands, with_mcp, counts_partial}(path counts summed where reported). Either is null when its events are absent;environmentis null when both are, and always null for Codex, which emits neither. OTel carries no tool-definition count or schema size, so neither is reported. The session id that keys the lookup is never returned.context:{points, missing, partial, compactions, limits}.pointsare{span, lane, start_ms, tokens}per LLM call in time order: Claude input + cache read + cache write, Codex the completion's input including cache (spannull when no sampling span matched).tokensis null when any part is unknown and counted inmissing;partialmarks a truncated Claude detail or a limited Codex turn set.compactions(Claude) are{start_ms, pre_tokens, post_tokens, trigger, success}of compaction events whoseprompt.idis a prompt of the task's calls, at most 100; null when no call has a prompt id, and always null for Codex.limits(Codex) is the latestcodex.conversation_startsof the conversation stored up to the turn's end (a start can be stored with the collector's later observed time), looking back 7 days:{context_window, auto_compact_token_limit}, null when none is reported; always null for Claude, which reports no context window.agents(Codex only): subagent turns linked through the trace's parent-span chain (a subagent turn'ssession_task.turn←op.dispatch.inter_agent_communication←dispatch_tool_call_with_state← the parent turn'scollaboration<tool>span), read from the traces of the task's spans between 10 minutes before the turn and 24h after it ends.parentis{id, tool, started_at, link}of the turn that started this one, else null (started_at/linknull when the parent's turn row is absent).childrenare up to 50 subagent turns, three levels deep, in tree order:{id, parent_id, depth, tool, started_at, duration_ms, models, llm_calls, cost_usd, cost_partial, link}:llm_callsis the subagent turn's model-call span count andcost_usdits completion-log estimate, folded like a list row (null when its turn row is absent). Each child'slink{from, to}is the UTC hour containing that turn's start (from its turn row, else its lineage span), for opening its own detail; the parent'slinkis the same for the parent turn. Subagent turns and completions are read in the detail's window (from - 24h..to + 24h): a linked subagent outside it is listed with unknown calls and cost and a partial total.subagentscounts their distinct conversations;task.subagentsis that count when positive, else null (a turn without links may lack traces or run a Codex version without collaboration spans).spawn_calls/unlinked_callscount the turn'sspawn_agent/followup_taskspans and those with no linked turn (null when spans are truncated).totalis{llm_calls, llm_calls_partial, cost_usd, cost_partial}over the turn and every child;limitedmarks a depth, count, trace or row bound. Traces only establish lineage: calls remain model-call span counts and costs completion-log estimates.activity(Codex only):{skills, skills_limited, tools, tools_partial}.skillsare the turn'scodex.skill_invocationlogs (exactconversation.idandturn.id) grouped as{name, invocation: explicit|implicit|null, scope, calls}, at most 50 (skills_limitedpast that);name/scopeare null unless label-shaped, and plugin ids are not returned.toolsare the returned spans carryingtool_name, grouped as{name, mcp, calls, duration_ms, duration_partial, aborted}by call count, at most 50;mcpmarksmcp__<server><tool>names (Codex joins server and tool, so they are not split) andduration_mssums known span durations, so concurrent calls add up beyond wall time (null when none is known). Skill events are deduplicated on timestamp and attribute maps first.tools_partialmarks a truncated detail or more than 50 tools. Empty lists mean nothing was recorded, not that nothing was used.
Both lookups are range-only over enabled clients: they reject team and ignore the other filters.
| Path | Fields and grain |
|---|---|
/api/teams |
Object {teams, sources, labels, historyStart, directory}: sorted distinct resolved teams of sessions with team evidence inside the range. sources maps each team to "directory", "device" or "both" (where its sessions' team came from). labels maps raw team values to display names (filters, grouping and URLs use raw values; display and CSV use labels), {} when no directory is configured. historyStart is the epoch-ms time history began (set once the selected attribute has stored history), or null. directory is true while directory teams are applied (an attribute is selected and its history is within the bound); the UI marks device-provided teams only then. |
/api/users/teams |
Object {users}: per raw user identity (email, else enduser.id), {user, team, teams, sources} with every resolved team of the user's sessions with in-range team evidence, most recent first; team is the first; sources[i] ("directory", "device" or "both") is where teams[i] came from. Users without team evidence are absent. IDs are raw like other user APIs; UI masking is not access control. |
Logs are retained 90 days (data retention), so older counter-only sessions have no team.
Admin-only (settings:manage); responses are Cache-Control: no-store. In Basic mode the routes are
reachable but directory teams are unavailable, so reads report why and PUT never saves.
| Path | Behavior |
|---|---|
GET /api/settings/teams |
{available, reason, poolId, status, attributes, current, labels, options, directoryError, version}. available:false carries reason basic_auth, not_cognito or ingest_disabled (directory teams need AUTH_MODE=oidc, a Cognito OIDC_ISSUER and INGEST_TOKEN_TABLE). status is the sync state (lastSync, lastError, stale, users, historyStart, reason; reason directory_too_large means more than 4,000 history intervals, so the directory is not applied). attributes is the timeline of selected attributes with their from times; current is the latest. options lists selectable Cognito attributes (string custom:*, name, derived email_domain). During a Cognito outage options is null with directoryError: "directory_unavailable" while stored settings stay readable. |
GET /api/settings/teams/preview?attribute= |
{attribute, values:[{value, users}], missing, excluded}: user counts per value for attributable users. missing has no value; excluded has no usable (ASCII) email or shares an email with another user; such users get no new history, so sessions not covered by history stored earlier use device teams. Errors: invalid_attribute (400), directory_unavailable (503; 409 when unavailable). |
GET /api/settings/display |
settings:manage. {available, reason, teamMask:{deployment, override, effective}, version}. override is true, false or null (follow the deployment). available:false (same reason values as team settings) means no settings store: only the deployment default applies. Reads the store directly; store_unavailable (503) when it fails. |
PUT /api/settings/display |
Body {teamMask, version} with teamMask true, false or null and version the last read value (null before the first save). Returns the GET view. Errors: invalid_team_mask, invalid_version, invalid_json (400), version_conflict or settings_unavailable (409), store_unavailable (503), body_too_large (413, body over 4 KiB). |
PUT /api/settings/teams |
Body {attribute, labels, version} (at most 16 KiB; version is the last read value, null for the first save). Saves the attribute and labels and triggers a sync; returns the same view as GET. Errors: invalid_attribute, invalid_labels, duplicate_label, invalid_version (400), version_conflict (409), directory_unavailable (409 when unavailable, 503 on a Cognito failure), store_unavailable, body_too_large (413), invalid_json (400). |
KPI rows also expose commits_observed, prs_observed, and lines_of_code_observed.
Active-time rows expose separate user/CLI observation flags. These require in-window
samples with matching metric/type identities; lookback-only counters do not establish zero.
Above four hours, the first hour uses raw evidence and later hours use rollup presence.
Live ends retain the trailing partial hour, whose samples can extend beyond the requested
end; historical ends use the usual alignment. These flags inherit that approximation.
The flags establish observation, not complete collection. Detail Productivity and Executive
withhold aggregate outcome/time values if any returned channel lacks the relevant evidence.
See activity availability.
Primary spend consumers select reported costs through frontend
spend.js. Original cost or summary computed_cost remains
a token-price diagnostic. The API does not add display-status fields globally. Missing
reports and zero reports with positive tokens are unpriced at spend consumers; positive
aggregates are not proof of complete capture. See metrics.
| Path | Fields and grain |
|---|---|
/api/cost/summary |
Per group: computed_cost, strict reported_cost, additive known_reported_cost and reported_partial, token fields, unpriced_tokens, sessions; includes unknown-channel rows by default. |
/api/cost/by-model |
Per group, model: priced row plus tokens, known_reported_cost, reported_partial, cost_counter_rows, and observed_usage_rows; empty model stays model="". |
/api/cost/by-user-model |
Per user, group, model: priced row plus tokens; nonempty email required; empty model stays model="". includeUnknown=1 broadens channel coverage. |
/api/cost/by-model-daily |
B, per day, group, model: priced row plus the report coverage fields below. The field is named day even for sub-day buckets. |
/api/cost/by-team |
Per session team (the team semi-join resolution): team (null = no team evidence, last), known_reported_cost, reported_partial, reported_cost, computed computed_cost, token subtotals and tokens, unpriced_tokens, users, sessions, requests. A total metric: the unknown channel is included, so Σ rows matches the /api/cost/summary known total for the same filters when every amount is known. A non-Anthropic model without a usable factor (ADR-017) makes only the affected team's slice unknown, while the summary loses the whole channel and model amount, so the team known sum can then exceed the summary's. Rows need in-window samples: a team seen only in the counter lookback has no row. sessions counts distinct sessions with in-window usage (not claude_code.session.count); requests counts api_request events in [from, to). A team filter returns that team only. |
/api/cost/by-user-daily |
B, per day, group, user: the same per-(model, session) report scopes, each attributed to one user (max(UserEmail), so a non-empty address wins; "" means no identity), regrouped by user. The top 20 users by known report over the whole requested range are separate, and "" always is; the other users fold into one user: null row per day, group, whose rest_users is the range-wide count of folded users (constant across rows). Report coverage fields and tokens, no computed cost/unpriced (a user row mixes models). |
/api/cost/by-team-daily |
B, per day, group, team: the same per-(model, session) report scopes attributed to each session's team, regrouped by team. The top 8 teams by known report over the whole range are separate; "" (no team evidence) always is and is never folded; the rest fold into one team: null row per day, group with the range-wide rest_teams count. Includes the unknown channel (a total metric), so buckets are not comparable one-to-one with /api/cost/by-model-daily. Report coverage fields and tokens, no computed cost/unpriced. |
/api/cost/by-model-compare |
Per model across selected channels: current priced-row fields, prev_reported_cost, previous token fields prefixed prev_, and computed prev_cost; previous period derived server-side. |
/api/cost/tiers |
Object {bedrock, enterprise}, each with computed uncachedInput, cacheRead, cacheWrite, output; unknown rates are skipped. |
/api/cost/effort-mix |
Per group, effort: computed cost, strict reported_cost, additive known_reported_cost and reported_partial, tokens, unpriced_tokens; empty effort becomes unknown. |
/api/cost/by-agent |
Per group, agent: the same cost/coverage fields as Effort; empty agent becomes main. Returns the top 30 by computed cost after aggregation. |
Summary, model, Effort, agent, and efficiency known_reported_cost retain usable model totals
inside each channel and endpoint dimension, with
reported_partial when other model totals are unavailable. All-unpriced evidence
remains null. Summary/model/Effort/agent zero requires an in-window cost counter
and no positive tokens in that model scope; session-count-only and lookback-only
rows cannot establish that zero. Efficiency retains the existing per-user/model
report eligibility, including its zero policy without observation metadata.
Effort/agent model scopes without an in-window cost or token sample are omitted;
pre-window samples still establish counter baselines.
Current token samples with zero increments and no cost sample remain unavailable
coverage; they mark a mixed Effort/agent subtotal partial rather than establish zero.
The model endpoint retains its legacy rows and strict values. Its observation fields
count folded series with current cost samples or any current cost/token samples,
respectively, not raw exports. They use the existing raw/rollup window boundaries.
Cost omits explicitly historical-only model rows before computing current model shares.
The Claude detail Cost page uses these subtotals, discloses partial chart totals,
lists unavailable chart scopes, and exports cost status for its tables.
Summary-derived averages retain partial labels; the API's efficiency
/api/cost/by-model-daily judges report availability per session inside ClickHouse
(costByModelDailySql in queries.js) and returns one row per
day, group, model (including empty models); modelCostTrend.js maps the counts.
A session report is usable when it is positive, or zero with known zero tokens or no token
samples. reported_cost is the sum of usable reports and null when no session is usable;
reported_partial, reported_unavailable (unusable sessions), reported_reasons and
reported_all_unavailable disclose the exclusions. report_missing counts sessions with no
report sample or an invalid (negative or non-finite) report; report_zero_with_tokens counts
zero reports beside positive or unknown tokens; together they equal reported_unavailable.
A session with no in-window cost or token sample, such as a baseline-only first bucket, is
omitted as no data rather than reported as a known $0. Minute buckets are clock-aligned and
only buckets starting at or after from are returned, so an unaligned from drops the
partial first minute and any increment reported in it (a pre-existing detail boundary; the
coding-client views split that minute instead). A report-only session adds 0 to the
canonical token sums; observed_tokens excludes it and tokens_partial is the coverage
signal (all report-only yields null). observed_tokens and tokens_partial follow
ADR-014; computed cost prices the summed
tokens of each model, which equals the sum of per-session estimates, and is null for unpriced
models. More than 50,000 day, group, model rows return 400.
The Cost UI reorders the returned agent subset by reported spend and shows at most 15; this is not a fleet-wide top-15 query by reported spend. Its computed comparison is opt-in. Efficiency ratios are null when reports are unpriced or denominators are zero, independently of server rate coverage. Token-tier dollars are estimates under the configured cache-write TTL, not an exact allocation of reported spend.
| Path | Fields and grain |
|---|---|
/api/adoption/levels |
Object total_members, mau, wau, dau; nonempty emails on session-counter rows before to. Members covers retained history; active windows are 30, 7 and 1 days. |
/api/adoption/timeseries |
Daily t, dau, wau, mau, stickiness; UTC day unions over trailing 1/7/30 days. stickiness is a percentage (0-100), not a fraction. |
POST /api/chat takes JSON history and returns SSE, not a JSON row array:
{"messages":[{"role":"user","content":"Summarize reported spend for the last day."}]}It returns 503 unless auth is configured (or CHAT_ALLOW_INSECURE=1) and
assertReadonlySession() has confirmed a readonly ClickHouse session. The probe runs at
startup and every ten minutes; unknown status is fail-closed. Per-process IP limits return
429 before streaming. SSE events are status (message, optional sql), thinking
(text), text (text), chart, done ({}), or error (message). Once streaming starts,
failures are SSE events on HTTP 200. Successful streams use Cache-Control: no-cache.
Concurrent chat overload returns 503 before SSE without consuming an IP rate slot.
chart carries a fixed bar|stacked_bar|line template, title, category/time column
x, x_type, one to three series (key, label), unit, and at most 50 data
rows selected from a stored query result. source, coverage, cost_bases,
partial, truncated and
optional effective_range describe its evidence. Null values remain unavailable.
The dashboard web validates each event again, renders the template without executing
model-generated code and excludes chart arrays from subsequent model history.
See chat limits and runtime admission.
GET /api/insights/daily?period=YYYY-MM-DD reads stored daily AI insights (Claude Code only;
404 when Claude is disabled). It returns object {enabled:false} when the feature is disabled
or misconfigured. period must be one of the last seven UTC days, otherwise 400; the default is the
latest stored period, else the previous day. A stored period returns enabled, status: "ready",
period, generatedAt, model, empty (no sessions, no model call), summary,
findings[{user, pseudonym, title, detail, metric}], caveats, org (period totals and the
seven-day daily-average baseline), users[{pseudonym, user, ...aggregates}],
usage{inputTokens, outputTokens, estimatedCostUsd} and available (stored periods). Otherwise it
returns {enabled:true, period, status, available} with status pending (the previous day before
success or exhausted attempts) or failed. user is the raw email mapped from the stored pseudonym;
the UI masks it. Model text is untrusted plain text and can be wrong; the card's numbers come from
the stored aggregates. Operations: daily insights.
| Path | Response |
|---|---|
/healthz |
HTTP 200 {ok:boolean} from ClickHouse ping, even when false. |
/readyz |
{ready:boolean}, HTTP 200 when ready, 503 when draining or ping fails. |
/api/health/data |
{status, latest, ageMinutes, staleAfterMinutes}; status is ok, stale or unknown. HTTP 200 only for ok, 503 otherwise. |
/api/config |
{piiMask, teamMask, pricing, schema, groupMode, defaultRangeDays, rangeCapDays}; reads in-memory configuration/probe snapshots and the in-memory price catalog status, with no ClickHouse query. teamMask comes from a per-replica cache; when it is older than 30 seconds the handler starts one DynamoDB read, waits at most 500 ms for it, then answers from the cache while a slower read finishes in the background. |
Freshness uses the maximum enabled-source timestamp (Claude raw metrics, Codex logs)
over seven days, memoized for 30 seconds; one fresh source can mask another's outage.
latest is ISO 8601 or null; ageMinutes is an integer or null. DATA_STALE_MINUTES
defaults to 360 and must be positive and below 10,080. Missing or failed measurement is
unknown, not healthy. Process shutdown and manifest probes are described in
runtime.
Config fields:
piiMask: true for case-insensitivePII_MASK_ENABLED=1ortrue; otherwise false. Terraform defaults masking on. The frontend masks unless this response explicitly says false. Ordinary API payloads still contain raw identities.teamMask: effective team-name display masking: an administrator override saved throughPUT /api/settings/display, else the deployment defaultTEAM_MASK_ENABLED(1/true,0/false; unset followspiiMask). Each replica caches the override for 30 seconds, then rereads it (waiting at most 500 ms); a store outage keeps the last known value, and before a replica's first successful read the answer istrue(masked). The frontend masks team names unless this says false and falls back topiiMaskwhen the field is absent. Team values in API payloads stay raw.pricing:{cacheWriteTtl, overriddenModels, priceList}. TTL is1hby default or5mwhen configured; invalid values fail startup. Override names come fromPRICING_JSON. Rates are not exposed. Cost displays this TTL as an assumption in computed diagnostics.pricing.priceList:{source, fetchedAt, models}for the active AWS Price List layer (ADR-017 amendment).sourceisruntime(latest successful in-memory refresh),snapshot(the image'sawsPriceSnapshot.json) orcode(neither; code tables only).fetchedAtis the layer's ISO 8601 fetch time or null;modelslists the model keys the layer prices, sorted. Operator overrides (PRICING_JSON,CODEX_PRICING_JSON) still win per model, and code tables price models the layer lacks. Metadata only: no rates. Each replica refreshes independently, so two replicas can briefly report different layers. The Cost page names the source in its computed-cost help.schema.segmentAwareSeriesKey: true, false or null from up to 2,000 newest cost datapoints within 24 hours. Mixed, absent or failed evidence is null; this does not certify old data or rollup rebuild completion.schema.migrations: sorted distinct ledger versions; null means undetermined/missing ledger, while[]means a readable empty ledger.schema.projectColumns: true afterSELECT ProjectName, Entrypoint FROM claude_code.otel_logs LIMIT 0succeeds; numeric server errors yield false, transport or other undetermined failures yield null. This checks logs only, not metric/trace columns or all migration-005 steps. All schema probes refresh every ten minutes.groupMode:aborsingle;defaultRangeDaysdefaults 2 andrangeCapDaysdefaults 90. Invalid startup settings are rejected, including a cap below the default range.
| Status | Contract |
|---|---|
| 400 | Wrapped route validation: {error, detail} for invalid range/interval or excessive span. The detail does not echo submitted values. |
| 401 | Missing/invalid Basic credentials or an OIDC session that cannot be renewed. |
| 403 | OIDC permission/group or unsafe-request Origin denied. |
| 429 | Chat's per-process IP limit. |
| 500 | Wrapped query failure: {error:"internal error", id:"<uuid>"}. The full exception is logged under the ID, not returned. |
| 503 | Not-ready/freshness states, unmet chat gates, or exhausted runtime admission. Overload JSON has code:"capacity_exhausted" and Retry-After: 1; other wrapped query failures remain 500. |
| 504 | The shared data-query load exceeded its cooperative 25-second budget: {error, code:"query_timeout", id:"<uuid>"} and Retry-After: 5. A warning logs the ID, registered route and code without filters or SQL. The localized message suggests a smaller range or retry. Timeout results are not cached as empty or zero data. |
After authentication, JSON API middleware sets no-store; the server's bounded promise cache is separate.
CloudFront configuration declares zero error-cache TTL for dashboard
403/500/502/503/504 without rewriting status or body. CloudFront can cache these errors
under other cache policies or positive origin cache directives; verify the actual policy,
error TTL and response headers. AWS error processing
documents that managed CachingDisabled disables cached 5xx errors; 400/403 caching
requires positive max-age or s-maxage.
The explicit zero-TTL entries protect this deployment contract if its policy changes.
There is no general data-route rate limit or pagination contract. Named row caps above are
part of the returned subset. See API implementation for cache and warmer
behavior, and security for SQL and identity boundaries.
For an authenticated local port-forward, set BASIC_AUTH_USER to your authorized username.
curl prompts for the password instead of placing it in the command arguments:
curl --fail-with-body --get http://localhost:8080/api/cost/by-model \
--user "$BASIC_AUTH_USER" \
--data-urlencode 'from=2026-09-01T00:00:00Z' \
--data-urlencode 'to=2026-09-03T00:00:00Z' \
--data-urlencode 'group=bedrock'Outside a local port-forward, use the deployment's authorized HTTPS origin.