Tenant Metrics & Usage Tracking
Overview
The platform records the tokens every model call consumes, and the portal reads the records back as usage views. The portal shows token usage per tenant, model, and API key. Per-user breakdowns are available through the API. The same records feed the API key usage export.
Key Concepts
What Gets Tracked
The platform automatically tracks the following metrics:
- Prompt Tokens (input): Tokens sent in requests
- Completion Tokens (output): Tokens returned in responses
- Embedding Tokens: Tokens used for embedding operations
- Rerank Tokens: Tokens used for reranking operations
- Total Tokens: Every metered token: prompt, completion, embedding and rerank tokens added together.
- Audio Duration: Length of transcribed audio, in minutes, as the model's backend reports it from the decoded audio. It is not a token count, so it has its own chart and never joins a token total or a ranking.
Transcriptions count as prompt and completion tokens, like chat, when the model reports them; a plain-text
response_format returns no usage at all, and those requests stay out of token totals. A successful transcription
also records audio duration, kept out of every token total. The in-repo Whisper engine reports it for every format it
serves; other backends record it when their reply includes a duration. Streamed replies record no audio duration.
How It Works
Services log token usage to ClickHouse after each model call. The portal reads the records back over two services, each gated on the audience it serves:
cmind.metering.v1.PlatformUsageService: a platform administrator, across every tenant, optionally narrowed to some of them.cmind.metering.v1.TenantUsageService: a tenant administrator, inside their own tenant. The tenant comes from the credential, never from the request.
The platform views also read the service-mesh access logs, for the request volume and latency of every HTTP call the platform served, including calls that never reached a model.
Using the Usage Views
Tenant Usage
Tenant administrators reach their own usage through Usage in the tenant sidebar. The link is shown to tenant administrators only.
The page holds the time-range controls, the token usage explorer, the Requests card, and the export of token usage per API key.
Platform Usage
Platform administrators reach usage in Platform → Usage, which holds two tabs: Usage, for the token usage
explorer and the request counts, and Resources, for the cluster's live compute resources. The open tab is part
of the page address (?tab=), so a view is shareable.
The Usage Explorer
The explorer draws token usage over time as stacked bars or one line per group. Window totals shows Total, Input, Output, Embedding, and Rerank for every group in the selected period. Chart and table controls are independent.
Compare selects Models or Tenants for the platform chart. Use the picker beside it to search for and select up to 20 groups. Its label is Chart models or Chart tenants, based on what it selects. The chart starts with the 20 largest groups in the window, or all groups if there are fewer. Clear a selection to make room for another. Clearing every selection leaves the chart empty. The legend lists the groups drawn and their totals. Groups that stay selected keep their colour when selection or ranking changes. Chart switches between stacked bars and lines. If names cannot be loaded, the page uses IDs and offers Retry group names.
Group by belongs to the breakdown table. It offers Models, Tenants, Tenants, then models, or Models, then tenants on the platform view. The Filter models and Filter tenants controls apply before grouping and stay available with every grouping. All models and All tenants stay first in their lists, including during search. Select either option to remove that filter; Clear filters restores the complete table. Table filters have no 20-group limit. They do not change the chart, window totals, or CSV export.
The table displays 25 parent groups per page. Pair views start with the five largest children under each parent. Select Load 5 more to show the next children. Paging or loading more children pauses Live at the current window. Select Now to resume live updates. Parent totals include every matching child, including those not yet shown. Column headings sort parents across all pages. Children stay ordered by total tokens, largest first. Use the API or CSV export for complete data and further analysis.
If the chart cannot be loaded, select Retry chart. If a breakdown page cannot be loaded, select Retry table. A failed page change keeps the rows and page number from the last successful request. If more children cannot be loaded, the table keeps its current rows and shows Could not load more children. Try again. Select Load 5 more to retry.
The ordered table grouping (?groupBy=tenant,model, for example), chart dimension (?chartBy=), chart type (?chart=), chart selections
(?tenants=, ?models=, ?apiKeys=), and table filters (?tableModels=, ?tableTenants=, ?tableApiKeys=) are part
of the page address, so a view is shareable. The time range belongs to the page; every card follows the same exact
bounds. Zooming preserves both sets of controls. Chart selection does not change the CSV export.
A tenant administrator sees the same explorer on their own Usage page. Compare offers Models and API keys, with a Chart models or Chart API keys picker beside it. The table's Group by offers Models, API keys, API keys, then models, and Models, then API keys, with independent Filter models and Filter API keys controls. All API keys removes the key filter. Only usage inside the selected tenant is shown. API keys use their display names when available, or their IDs otherwise. No API key contains usage from requests made without an API key and can also be selected in the table filter.
The breakdown lists groups with usage in the selected period that match the table filters.
Time Range Selection
Preset ranges: Last hour, last 2 hours, last 6 hours, last 24 hours, last 7 days, Month to date and One month. A view opens on Last 2 hours. From and To stay visible for every preset.
Month to date runs from the first day of the current month to now, in your local timezone. One month covers the calendar month selected in Month. A completed month stays fixed, with Live off. The current month ends at now while Live is on. The Month list offers the current month and the eleven months before it. To view an older month, set From and To yourself.
Custom range: Edit From or To to switch to a custom window. Setting To stops Live at that instant. Now resumes live tracking; from a completed month, it moves to the current month. Pausing Live keeps the end time currently shown.
Zoom: Drag across the token graph to select a smaller time window. This pauses Live and updates every card. Reset zoom restores the window from before the first drag. The date fields provide the same time selection without dragging. Changing a time control clears the saved zoom window.
Live: On by default for a window that ends at now. Figures refresh every 30 seconds at the fastest, or once per bucket for wider buckets, up to 5 minutes. The breakdown keeps its sort order and keyboard focus during refresh.
Bucket width: Derived from the span, finer for a short window and coarser for a long one. The server chooses the width it actually returns and may coarsen the one it was asked for. Calendar months use sub-day buckets, so a UTC bucket is not presented as a complete local day. Bucket alignment does not extend the requested totals window.
Per-Tenant Usage (Portal)
Platform administrators can see how many tokens each tenant used: open Platform → Usage and set Group by to Tenants. The table under the bars has one row per tenant:
| Column | Description |
|---|---|
| Tenant | Tenant name, or the tenant ID when the name cannot be found |
| Total | Input, Output, Embedding and Rerank added together |
| Input | Tokens sent in requests |
| Output | Tokens returned in responses |
| Embedding | Tokens used for embedding |
| Rerank | Tokens used for reranking |
Tenants are listed with the highest total first, and clicking a column heading sorts by that column. Use Compare → Tenants, then Chart tenants above the chart to choose the tenants to draw. Tenants with no usage in the period are not listed.
Token counts are shortened, for example 1.2K for 1,200 and 3.4M for 3,400,000.
The table uses the same period as the rest of the page. When you change the period, or when Live is on, the table updates with it.
Understanding the Charts
X-axis: Labels follow the viewer's locale, like the other dates in the portal
- Interval of one day: The weekday and the date, without the year
- Window that covers more than one day: The weekday and the date at the first bucket and at the first bucket of each new date, even when these do not fall at midnight. Other ticks show the time. The chart shades alternate days and draws a line at each day change, with no line at the start of the window
- Window that stays in one day: The time
Hover label: Follows the viewer's locale. It carries the weekday and the date as well as the time when the period covers more than one day.
Y-axis: Auto-formatted token counts (K = thousands, M = millions)
Series: The explorer draws the selected groups in stable colours, with their totals in the legend. Window totals always include all groups. On an individual API key's usage tab, the lines instead show token types; select a type beside the chart to hide or show it.
Exporting API Key Usage
Tenant administrators can download token usage per API key as a CSV file.
Portal
- Open Usage in the tenant sidebar. The link is shown to tenant administrators only.
- Select a Time range preset, or set the period with From and To.
- Click Export API-key CSV.
The file covers the selected period, which is also included in its name, for example
api-key-usage-2026-08-01T000000Z-to-2026-08-06T000000Z.csv. Keys without usage in that period are omitted. See
API Key Usage Export for the columns, row structure, ordering, and attribution rules.
API Reference
API Key Usage Export
POST /cmind.metering.v1.TenantUsageService/ExportTokenUsageByApiKey
Download one CSV file with token usage for every attributed API key in a tenant. The caller must be a tenant administrator for that tenant.
Request Fields:
window(required): The time range, with astartand anend.endmust be afterstart.
The time range includes start and excludes end.
The request does not name a tenant. The tenant comes from the credential you call with: an API key belongs to one
tenant, and a user token must send the tenant ID in the X-Cm-Tenant-Id header. A call that names no tenant fails
with permission_denied.
Response Fields:
filename: Suggested filename, such asapi-key-usage-2026-08-01T000000Z-to-2026-08-06T000000Z.csvcsv: CSV file contents as bytes
CSV Columns:
| Column | Description |
|---|---|
period_start_utc | Start of the requested range, in UTC |
period_end_utc | End of the requested range, in UTC |
api_key_id | Stable API key ID, to distinguish keys with the same name |
model | Internal model resource name stored with the usage |
input_tokens | Tokens sent in requests |
output_tokens | Tokens returned in responses |
embedding_tokens | Tokens used for embedding |
rerank_tokens | Tokens used for reranking |
audio_duration_ms | Length of audio submitted for transcription, in milliseconds |
api_key_name | Current display name, as shown in the portal |
api_key_prefix | Current key prefix, as shown in the portal; never the full secret |
model_display_name | Current display name; empty when unset |
model_public_id | Public ID of the underlying model, used to invoke it directly |
The file has one row for every pair of API key and model. There is no summary row for a key and no total column. To
get a key's token total, add its rows and its four token columns together in a spreadsheet or in your own code.
audio_duration_ms is not a token count, so leave it out of that sum.
Rows are ordered by API key ID, and by model within each key. Revoked keys with usage are included. Key names and prefixes describe the key at export time, not at the time of each request. Model metadata also reflects the current model resource. The model fields are separate: an unset display name stays empty. Model display names and public IDs are empty when the caller cannot see the model in the selected tenant, the resource no longer exists, or a deployment and an external model have the same resource name. The stored resource ID and usage stay in the export. Usage through an alias is attributed to its underlying model. If a key is absent from the caller's key lists, its name and prefix are empty, but its ID and usage stay in the export.
The file uses UTF-8 with a byte-order mark and CRLF line endings so Excel can open names with non-ASCII
characters. This changes the file's leading bytes: programmatic readers must use a BOM-aware decoder, such as
Python's encoding="utf-8-sig", or remove the leading U+FEFF before parsing the CSV. Otherwise the first header can
be read as \ufeffperiod_start_utc instead of period_start_utc.
Text that starts with a spreadsheet formula marker has a leading apostrophe. Token counts and audio duration remain exact decimal integers.
The export is not paginated and has no row limit. It includes every API key with attributed usage in the requested range. If no key has usage, the file contains only the header row.
Only usage that the platform can attribute to an API key is included. Requests made without an API key are omitted, so the CSV totals can be lower than the tenant totals shown in the usage views.
The request fails with invalid_argument if window is missing, if its start or end is missing, or if end is
not after start.
The export reads current API key and model information before it creates a file with usage rows. If a lookup
fails, the request returns an error, such as unavailable, and no CSV file. For unavailable, try the export
again later.
Per-Tenant Token Usage
POST /cmind.metering.v1.PlatformUsageService/ListPlatformTokenUsage
Get token totals for one time range, broken down by tenant, model, user, or tenant/model pairs. The caller must be a
platform administrator and must call without selecting a tenant. A request that names a tenant fails with
permission_denied.
Request Fields:
window(required): The time range, with astartand anend.endmust be afterstart.groupByDimensions(required): An ordered list of one or two dimensions. Use["TOKEN_USAGE_GROUP_BY_NONE"]for one total. A single dimension can be"TOKEN_USAGE_GROUP_BY_TENANT","TOKEN_USAGE_GROUP_BY_MODEL", or"TOKEN_USAGE_GROUP_BY_USER". Only tenant and model can be paired:["TOKEN_USAGE_GROUP_BY_TENANT", "TOKEN_USAGE_GROUP_BY_MODEL"]returns tenant/model pairs; reverse the list for model/tenant pairs. Dimensions must be unique.NONEmust appear alone.tenantIds(optional): Tenant IDs to include. Leave it empty to include every tenant. This filter is independent of the grouping dimensions.page(optional): Cursor and page size. Paging always applies; the default is 25 parent groups, maximum 100.orderBy(optional): Parent sort field and direction. See Cursor paging.modelIds(optional): Models to include. Leave it empty to include every model.userIds(optional): Users to include. Leave it empty to include every user.
The time range includes start and excludes end. The result is not split into time buckets. Each row gets one set
of totals for the whole range.
Response Fields:
window: The time range the totals cover.page: Parent page cursors and continuation flags.parentGroups: For two dimensions, parents on this page with complete totals and achildPagecursor.groups: The single-dimension page, or up to five children per parent for two dimensions, with these fields:
| Field | Description |
|---|---|
groupKeys | One key per requested dimension, in the same order. A key is empty when usage has no attribution on that dimension. The list is empty for an ungrouped total. |
tokens.inputTokens | Tokens sent in requests |
tokens.outputTokens | Tokens returned in responses |
tokens.embeddingTokens | Tokens used for embedding |
tokens.rerankTokens | Tokens used for reranking |
tokens.audioDurationMs | Length of audio submitted for transcription, in milliseconds. It is not a token count. |
By default, parents are ordered by their four token counts added together, highest first. Audio duration is not part of that sum and does not change the order. Tenants with no usage in the range are not returned. The response contains tenant IDs, not tenant names.
Grouping by API key is not available here, because an API key belongs to one tenant. Use ListTokenUsage for per-key detail inside a tenant.
The request fails with invalid_argument if window is missing, if its start or end is missing, if end is not
after start, if groupByDimensions is empty, if a grouping is unsupported or has duplicate dimensions, or if a
value in tenantIds or userIds is not a valid ID.
GetPlatformTokenUsageSeries and GetTokenUsageSeries take one scalar groupBy, including
TOKEN_USAGE_GROUP_BY_NONE. Their responses include maxSeries: the server's maximum number of groups in a
comparison chart. The portal uses it to limit token-chart selections. It is also returned for NONE, so a picker
can read the limit with the window totals. Request-count charts use ungrouped queries and have no group picker.
The series limit does not cap breakdown filters or exports.
A grouped token series request returns at most maxSeries groups. If more groups have usage in the range, only
the largest by token total are returned. The other groups are omitted, so a group absent from a chart can still
have usage. Page through ListPlatformTokenUsage or ListTokenUsage to read every group in a breakdown.
Cursor Paging for Token Breakdowns
Both token breakdown endpoints always use cursor paging, even when page is omitted. Set page.pageSize to the
number of first-dimension groups to return: the default is 25 and the maximum is 100. To continue, send the response's
page.endCursor as page.cursor while page.hasNextPage is true. Keep the window, filters, and order fixed.
Late usage reports can change totals between requests; paging does not provide a snapshot.
The response's page contains startCursor, endCursor, hasNextPage, and hasPreviousPage. The cursors identify
the first and last groups on the page. To read the previous page while hasPreviousPage is true, send the current
response's page.startCursor as page.cursor with page.direction: "PAGE_DIRECTION_BACKWARD". Forward paging is
the default. Groups always return in the requested sort order, whichever direction you read in.
A cursor belongs to the sort order it was issued for. Using it with a different orderBy sort order returns
invalid_argument. Start a fresh traversal when you change the sort order.
With two dimensions, parentGroups contains the selected parents and their complete totals. groups contains
up to five children per parent. Each parent's childPage shows whether more children exist. To continue one
parent, filter by its key, group by the second dimension alone, and send its childPage.endCursor as page.cursor
with page.pageSize: 5 and orderBy: "total_tokens desc".
orderBy accepts total_tokens, input_tokens, output_tokens, embedding_tokens, or
rerank_tokens, followed by asc or desc. The default is total_tokens desc. Group keys break ties in ascending
order. Children always start in descending total-token order.
The former limit field is removed. Follow parent and child cursors to read the full breakdown. CSV exports remain
complete and do not use these page limits. A database timeout returns deadline_exceeded; exceeding the query
memory limit returns resource_exhausted. Use a narrower window or more filters before retrying those queries.
Token Usage Breakdown for One Tenant
POST /cmind.metering.v1.TenantUsageService/ListTokenUsage
Get token totals for one time range inside one tenant. The caller must be an administrator of that tenant.
The request has no tenant field. For a user token, send the tenant ID in the X-Cm-Tenant-Id header. The service
reads the tenant scope from the caller's credential. A request without a tenant fails with permission_denied.
Request Fields:
window(required): The time range, with astartand anend.endmust be afterstart.groupByDimensions(required): An ordered list of one or two dimensions. Use["TOKEN_USAGE_GROUP_BY_NONE"]for one total. A single dimension can be"TOKEN_USAGE_GROUP_BY_MODEL","TOKEN_USAGE_GROUP_BY_USER", or"TOKEN_USAGE_GROUP_BY_API_KEY". Only API key and model can be paired:["TOKEN_USAGE_GROUP_BY_API_KEY", "TOKEN_USAGE_GROUP_BY_MODEL"]returns API key/model pairs; reverse the list for model/API key pairs. Dimensions must be unique.NONEmust appear alone.page(optional): Cursor and page size. Paging always applies; the default is 25 parent groups, maximum 100.orderBy(optional): Parent sort field and direction. See Cursor paging.modelIds(optional): Models to include. Leave it empty to include every model.userIds(optional): Users to include. Leave it empty to include every user.apiKeyIds(optional): API keys to include. Leave it empty to include every key.
"TOKEN_USAGE_GROUP_BY_TENANT" is not accepted here, because the result already covers one tenant. Use
ListPlatformTokenUsage to compare tenants.
Response Fields:
window: The time range the totals cover. It includesstartand excludesend.pageandparentGroups: The same parent pagination and complete totals as the platform endpoint.groups: The single-dimension page, or up to five children per parent, with the samegroupKeysandtokensfields as Per-Tenant Token Usage. Each row covers the whole range. By default, rows are ordered by their four token counts added together, highest first. Audio duration is not part of that sum.
The request fails with invalid_argument if window is missing, if its start or end is missing, if end is not
after start, if groupByDimensions is empty, if a grouping is unsupported or has duplicate dimensions, or if a
value in userIds is not a valid ID.