Search

↑↓ to move↵ to openesc to closetop 6 documentation results

Book a demoTry TokenTriage

In development · Explore the product preview. Join the launch list or book a walkthrough.

Browse docs

Read your gateway's traffic and spend

Ask a hosted gateway what it served, what it cost and how much of the answer is missing, from the CLI or either SDK.

How-to guideLast updated

On this page

Your application has been sending requests through a hosted gateway for a day. You want four things back: how many requests it made, how they ended, how long they took, and what the upstreams charged for them.

Three clients ask that question and read the same document: the tokentriage cloud CLI, the TypeScript SDK and the Python SDK. This page asks it once in each, then spends most of its length on the awkward parts of the answer: a cell with no figure in it, a table that was capped, a saved view this build can no longer answer.

You need an organization id (org_…), a workspace id (t_…), a management key in TOKENTRIAGE_MANAGEMENT_KEY, and metadata.read on the scope you ask about. Any money figure also needs cost.read. There is no database to stand up and no browser session: these reads go to the API the portal itself uses. Who can do what in your company covers how a grant reaches a person.

One request at a time is a different read: tokentriage cloud request get --request lreq_… (and client.requests.get(org, ws, req) in either SDK) answers what served that single request — gateway, environment, release or saved draft revision, purpose, the acting credential by label, the token usage, the cost with its pricing state, what the guards did, and, for a hosted test, the run, case and arm it is evidence for. A request id from another workspace is answered “not found” rather than “forbidden”, and a cost nobody has priced yet reads unpriced rather than $0.00.

This page does not cover saving a view, editing one, or sending app feedback. Those are write operations, and they have their own pages: Save a gateway view and share it and Report and read app feedback. How to read a cost figure covers where a dollar amount comes from in the first place; Gateway insight reads is the field-by-field contract behind this page.

Every block below is program output. The commands ran against a loopback server replaying sdk/spec/insight-read-fixtures.json, the recording the lane’s own handler wrote, so --base-url and the request line name 127.0.0.1 and a port that changes per run. Yours name your deployment.

Set the overview population

Open a gateway’s Overview and choose its environment, time window, activity purpose, and release. Application traffic is the default; playground tests, comparison runs and judge calls are separate choices. Each choice changes the overview, previous-period comparison, time series, release markers, model/route/release facets and feedback coverage together. A saved question retains that same population.

The release menu uses releases returned for the selected gateway and environment. It shows at most the first 100 available choices and discloses when more exist. All releases still queries the complete authorized population; choosing a release narrows the server query, rather than filtering the displayed page. Changing environment clears the previous environment’s release selection. If release choices cannot be read, the page explains that failure and retains the current filter.

A chart’s View as table button exposes its underlying measured values and missing-value reasons. It works with the keyboard. A facet opens the Requests tab with its time, purpose and release context; any additional filter applied only to a bounded page is labeled as such. Missing coverage or an unpriced population remains visible after filtering. A smaller population does not establish savings or repair a journal gap.

Read the current workspace budget

The overview’s Workspace budget card reads the enforcing authority separately from the traffic charts. Check its enforcement state, current reset window, limit, committed capacity, and remaining capacity. Change the chart dates without treating the budget as a historical balance. Use Refresh budget for a fresh authority read.

Committed capacity includes escrow; it is not settled provider spend. The authority does not separately report pending and reserved amounts, so those read Unknown. Missing access or a failed read does not mean there is no cap. The card requires workspace cost.read, the same permission as the supported budget API.

Review the attention cards

The overview shows at most four cards derived from the displayed evidence: the largest observed model spend, requests that did not complete, response p95 above your display threshold, and missing price or comparison quality evidence. Enter an optional threshold in milliseconds below the charts. It applies to this open view only; it does not configure an alert or change routing. No latency card appears until you set a positive threshold.

Use Latency distributions to read exact p50 and p95 timings with their sample counts. Expand Separate outcome populations to compare successful, refused, failed, cancelled, and partial requests. Cancellation comes from the recorded delivery outcome and is counted separately from failure.

Each timing shows observed and eligible requests. First-output timing applies to streams; an ordinary request supplies no first-output sample. A missing duration stays unavailable, while an explicitly recorded 0 ms remains a measurement. Older records without duration provenance can make a population partial. Its percentiles describe only observed samples.

Follow each card’s link to inspect the figures behind it. Partial costs can change the spend ranking, and incomplete coverage can move a percentile in either direction. Missing cost is never zero. The quality card reflects the selected saved comparison; changing scope or selecting an incomplete run removes any claim of complete evidence.

Read saved cost and quality evidence

In Overview, choose a saved comparison and a terminal run. The card reads the saved run’s results, including the sample version, both configuration pins, the acceptance rule and its quality counts. Baseline, candidate and judge spend remain separate from the sample’s measured cost reduction. A running run shows its status until you refresh.

The card also checks whether the recorded baseline is still the applied release. A changed baseline leaves the historical measurements readable and labels their applicability. A failed read offers a retry; changing the environment clears evidence from the previous scope.

For Application traffic, the same selected window and release can supply a qualified cohort projection. A cohort is the set of requests that match the saved comparison’s frozen input and configuration conditions. Read its covered and uncovered counts, tested pairs, assumptions and price basis beside the projected difference. A negative difference means a projected cost increase. It is not realized savings or a monthly forecast.

Other activity purposes do not request an application projection. Missing qualification, unpriced requests or an unavailable reader produce a reason, not a zero. The sample’s observed spend remains a different measurement. The comparison guide shows the read-only CLI and SDK calls for the same evidence.

Run a scoped read against your deployment

The shipped sdk/ts/examples/read_gateway_insights.ts and sdk/python/examples/read_gateway_insights.py read an overview, then one bounded page of requests and attempts under the same server-issued read context. Set TOKENTRIAGE_BASE_URL, TOKENTRIAGE_MANAGEMENT_KEY, TOKENTRIAGE_ORG, TOKENTRIAGE_WORKSPACE, TOKENTRIAGE_APPLICATION, TOKENTRIAGE_GATEWAY, TOKENTRIAGE_ENVIRONMENT, TOKENTRIAGE_FROM and TOKENTRIAGE_TO. The last two are a half-open UTC window. Set optional TOKENTRIAGE_RELEASE to narrow all three example reads to one release; the CLI equivalent is --release rel_… (repeatable to select several releases). The examples preserve the complete answer, including numeric strings, coverage, cursors and truncation; they do not add missing groups or convert unknown costs to zero. Neither example writes or executes a provider request.

Both programs and the shipped CLI were executed against the private customer API on September 23, 2026, for an existing one-minute A1 population. The overview and both drill-downs agreed on two requests, two primary attempts and 306 tokens. The two priced request receipts sum to 2,685,000 nano-USD ($0.002685), matching the overview, model/release facets and hourly bucket. Those answers still disclosed ingest_gap:tt-prod-pool; the exact visible rows do not establish that the whole source is complete. This is a read-only reconciliation, not new provider traffic.

Ask for an overview

An overview answers one value per metric over one half-open UTC window, grouped by activity purpose. Purposes stay in separate groups: a production request and a playground experiment are two populations, and their sum describes neither. --purpose application is the default.

$ tokentriage cloud insights overview \
    --metric requests.v1 --metric provider_spend.v1 \
    --metric cost_per_1000_requests.v1 --metric tokens.v1 \
    --metric response_latency.v1 \
    --org org_K11111111111 --workspace t_K11111111111 \
    --from 2026-09-20T12:00:00Z --to 2026-09-21T12:00:00Z \
    --base-url http://127.0.0.1:2091
application	requests.v1	1284 count
application	provider_spend.v1	unavailable (unpriced_population)
application	cost_per_1000_requests.v1	unavailable (unpriced_population)
application	tokens.v1	84210773 tokens (FLOOR: ingest_gap:gateway_journal)
application	response_latency.v1	412 milliseconds

1,284 requests, and no spend figure. That pairing is the subject of the next section.

Three lines went to stderr in the same run, so --json | jq keeps working and the qualifications still arrive:

cloud: customer.insights.query POST http://127.0.0.1:2091/portal/api/v1/customer/v1/organizations/org_K11111111111/workspaces/t_K11111111111/insights/query
cloud: coverage is incomplete for this window (ingest_gap:gateway_journal); measured values use an incomplete population; counts/sums are lower bounds, ratios/percentiles may move either way
cloud: gateway_journal is behind (produced 918290, indexed 918273), so the most recent traffic may be missing

The second and third lines are the answer describing its own gaps: some journal rows for this window have not been indexed yet, so the token figure is a floor, and the producer is 17 sequence numbers behind at the moment of the read.

The same question from either SDK. insights.query takes the query document, and the answer arrives under the envelope’s data:

answer = client.insights.query(org, workspace, {
    "schema": 1,
    "shape": "overview",
    "from": "2026-09-20T12:00:00Z",
    "to": "2026-09-21T12:00:00Z",
    "purposes": ["application"],
    "metrics": ["requests.v1", "provider_spend.v1", "cost_per_1000_requests.v1",
                "tokens.v1", "response_latency.v1"],
})["data"]

for group in answer.get("groups", []):
    for cell in group["cells"]:
        value = cell["value"]
        if value["state"] == "unavailable":
            print(cell["metric"], "is unavailable:", value["reason"])
        elif value["state"] == "partial":
            print(cell["metric"], value["number"], value["unit"], "FLOOR", value["coverage"])
        else:
            print(cell["metric"], value["number"], value["unit"])

Both print the same five rows, one per cell, differing only in how each language renders a list of strings:

requests.v1 1284 count
provider_spend.v1 is unavailable: unpriced_population
cost_per_1000_requests.v1 is unavailable: unpriced_population
tokens.v1 84210773 tokens FLOOR ['ingest_gap:gateway_journal']
response_latency.v1 412 milliseconds

Every cell names its unit, and the unit is one of the eight the contract carries — response_latency.v1 answers in milliseconds, the spelling the catalogue publishes for it. A formatter may be keyed off the unit; a ninth spelling cannot reach it.

Read a cell that has no number in it

A measure comes back in one of three states, and a renderer that treats them alike is how a bill gets under-reported.

state carries read it as
available number and unit a figure this service stands behind
partial number, unit and coverage a floor: the truth is larger, for the listed reasons
unavailable reason, and nothing else there is no figure to plot; the reason says what to do

The overview above is unavailable{unpriced_population}: rows exist, and some of them carry no price, so a sum over them would be smaller than the spend. Wait for pricing to land, and the cell fills in on its own.

A different reason on the same cell needs a different person. Reading it as somebody who holds metadata.read and not cost.read:

$ tokentriage cloud insights overview \
    --metric requests.v1 --metric provider_spend.v1 \
    --metric response_latency.v1 \
    --org org_K11111111111 --workspace t_K11111111111 \
    --from 2026-09-20T12:00:00Z --to 2026-09-21T12:00:00Z \
    --base-url http://127.0.0.1:2096
application	requests.v1	1284 count
application	provider_spend.v1	unavailable (scope_not_authorized)
application	response_latency.v1	412 milliseconds

The read itself went through. Only the cell that would disclose money is withheld, and the reason names the remedy: ask an administrator for cost.read, rather than waiting for a figure that is not coming. Treat every cell’s reason separately, and do not collapse an absent cell into a failed request.

A window in which nothing happened is a third thing again. There are no groups to iterate, and the CLI says so in a sentence:

$ tokentriage cloud insights overview \
    --metric requests.v1 --metric provider_spend.v1 \
    --org org_K11111111111 --workspace t_K11111111111 \
    --from 2026-09-20T12:00:00Z --to 2026-09-21T12:00:00Z \
    --base-url http://127.0.0.1:2099
no traffic in this window for the purposes asked; this is an ABSENCE of rows and not a measurement of zero

In either SDK the same answer is a data document with no groups member: "groups" in answer is false while read_context is still there to continue from. Synthesizing a row of zeros at that point publishes a measurement nobody made.

Read a money figure without losing digits

When every row in the window is priced, the spend cell fills in, and the CLI prints dollars beside the integer it was computed from:

$ tokentriage cloud insights overview \
    --metric requests.v1 --metric provider_spend.v1 \
    --metric cost_per_1000_requests.v1 \
    --org org_K11111111111 --workspace t_K11111111111 \
    --from 2026-09-20T12:00:00Z --to 2026-09-21T12:00:00Z \
    --base-url http://127.0.0.1:2094
application	requests.v1	1284 count
application	provider_spend.v1	$1234.567890123 (1234567890123 nano_usd)
application	cost_per_1000_requests.v1	$0.961500693 (961500693 nano_usd)

Money travels as a decimal string of integer nano-USD, and the string is the figure. Keep it as a string, or move it into an integer type that is not floating point. Nano-USD passes what a double holds without loss at about $9,007,199 of spend, which a large customer reaches inside a month; from there, two spends one nano apart compare equal.

spend = answer["groups"][0]["cells"][1]["value"]
print(spend["number"], spend["unit"], type(spend["number"]).__name__)
print("exact:", int(spend["number"]) * 2)
# 1234567890123 nano_usd str
# exact: 2469135780246

Drill into the requests behind a number

A drill-down returns the rows themselves. --grain request is one row per logical request, with retries and fallbacks folded into the request they belong to; --grain attempt is one row per dispatch, including the fallbacks that were discarded. No prompt text, no completion text and no money travel at either grain.

$ tokentriage cloud insights drilldown --grain request \
    --org org_K11111111111 --workspace t_K11111111111 \
    --from 2026-09-20T12:00:00Z --to 2026-09-21T12:00:00Z \
    --base-url http://127.0.0.1:2101
lrq_K11111111111	2026-09-21T11:22:00Z	completed	gpt-4o-mini	2140ms	first-output 318ms	attempts 2
lrq_M11111111111	2026-09-21T11:29:00Z	completed	claude-haiku	884ms	first-output -	attempts 1
lrq_N11111111111	2026-09-21T11:48:00Z	refused	-	41ms	first-output -	attempts 0

Read the second row’s first-output - as an absence. That request did not stream, so it produced no time-to-first-output sample; 0ms would have made it the fastest row in the table and pulled every first-output quantile down with it. The third row is a refusal before any upstream was dispatched, which is why it has no model and no attempts, and it still counts as one logical request.

One line went to stderr, and it changes what the table means:

cloud: this answer was TRUNCATED at a row bound; it is a page of the rows and not all of them

A capped table presented as a whole one is the table somebody makes a decision from. In the SDKs the same fact is answer["truncated"], and the rows carry first_output_known rather than a sentinel time:

for row in answer["requests"]:
    first = f"{row['first_output_ms']}ms" if row["first_output_known"] else "-"
    print(row["logical_request_id"], row["status"], row["total_ms"], first)
print("truncated:", answer.get("truncated", False))
# lrq_K11111111111 completed 2140 318ms
# lrq_M11111111111 completed 884 -
# lrq_N11111111111 refused 41 -
# truncated: True

Find out what you are allowed to ask

The catalogue lists every metric this deployment publishes, what unit it answers in, whether reading it needs cost.read, and whether anything writes it yet. It is answered from the server’s build and reaches no warehouse, so it works identically where the analytics producer is switched off, which makes it the thing to build a metric picker from.

$ tokentriage cloud insights catalog --org org_K11111111111 \
    --base-url http://127.0.0.1:2103
requests.v1	count	produced	metadata.read	How many requests did this application make?
provider_spend.v1	nano_usd	produced	metadata.read + cost.read	What did this application's upstream usage cost?
cost_per_1000_requests.v1	nano_usd	produced	metadata.read + cost.read	What does a thousand requests cost?
tokens.v1	tokens	produced	metadata.read	How many tokens did the upstreams read and write?
response_latency.v1	milliseconds	produced	metadata.read	How long did a request take end to end?

Those are five of the twenty-four rows that run printed; the full listing, and the closed enums that follow it on stderr, are in Gateway insight reads. A row whose producer_status reads no_producer is nameable today and answers unavailable (no_producer) until its slice lands, so a picker can show it disabled rather than pretend it is missing.

Open a saved view somebody kept

A saved view stores a question. It stores no SQL and no grant, so opening one re-resolves the reader’s own scope and answers under that: a view shared with a team shows each member what they may read.

$ tokentriage cloud view list --org org_K11111111111 \
    --workspace t_K11111111111 --base-url http://127.0.0.1:2105
sview_K11111111111	Checkout spend, last 24h	team	fixed	overview	revision 1
sview_M11111111111	A question this build lost	team	fixed	overview	revision 1

view get prints the stored question without answering it. view open returns both the question and the answer in one document, so the chart and the caption above it cannot come from two different moments of your access:

$ tokentriage cloud view open --org org_K11111111111 \
    --view sview_K11111111111 --base-url http://127.0.0.1:2109
sview_K11111111111	Checkout spend, last 24h	team	fixed	overview	revision 1
application	requests.v1	1284 count
application	provider_spend.v1	unavailable (unpriced_population)
application	cost_per_1000_requests.v1	unavailable (unpriced_population)
application	tokens.v1	84210773 tokens (FLOOR: ingest_gap:gateway_journal)
application	response_latency.v1	412 milliseconds

In the SDKs the opened document splits into view and answer, and the answer’s cells are the ones from the section above:

opened = client.saved_views.open(org, "sview_K11111111111")["data"]
view, answer = opened["view"], opened["answer"]
print(view["name"], view["audience"], view["time_intent"], view["revision"])
# Checkout spend, last 24h team fixed 1

When the read is turned away

Three refusals reach a caller of these operations, and they send you to three different places. Keep them apart.

What you see What happened What to do
HTTP 403, type=forbidden the scope names something your bindings do not reach ask for the grant; retrying changes nothing
HTTP 503, type=coverage_unknown the ingest side cannot bound what is missing for this window retry once the frontier has moved
HTTP 503, type=dependency_unavailable the analytics service could not be reached for this read retry, and quote the request id
HTTP 422, type=validation_failed a saved view asks something this build no longer publishes edit the view’s question

The 403 and the 503 look nothing alike in a terminal, which is the point of separating them: a surface that printed one sentence for both would send a customer to an administrator during an outage.

customer.insights.query was refused (HTTP 403); detail: foundation: forbidden: metadata.read is not permitted at application:app_M11111111111; type=forbidden; request_id=req_forbidden_foreign_application

customer.insights.query was refused (HTTP 503); detail: the ingest side cannot bound what is missing for this window; retry once the frontier has moved; type=coverage_unknown; request_id=req_coverage_unknown_503

Both SDKs make the same split into two classes, and carry the remedy the server’s own catalogue publishes for each code:

forbidden: InsightForbiddenError 403 forbidden refresh_access
unavailable: InsightUnavailableError 503 coverage_unknown retry

The 422 is the odd one. A view whose stored question this build cannot re-ask still reads perfectly through view get, and fails on open:

customer.saved_views.open was refused (HTTP 422); detail: foundation: unsupported combination: saved view sview_M11111111111 asks a question this build cannot re-ask: insightcontract: metric version unknown; type=validation_failed; request_id=req_saved_view_open_stale_question

It is not a 404 and not a 403: the view is there, and you are allowed to read it. The remedy is to edit the question.

Which class the SDKs raise for that 422

validation_failed is in the lane’s published table with next_action: check_selection, so both SDKs raise the BASE class for it — InsightRefusalError — rather than InsightForbiddenError or InsightUnavailableError. It is neither a permission problem nor an outage: catching the base class gets you the published remedy, and type still reads validation_failed.

What these reads do not answer

Three metrics in the catalogue carry producer_status: no_producer: test_quality.v1, paired_reduction.v1 and attention_rules.v1. Naming one in --metric is valid, and the cell answers unavailable (no_producer) rather than a zero, so a dashboard can show the question and say what is missing.

budget_standing.v1 carries producer_status: produced_elsewhere. Read its current control-plane standing through the budget operation; generic insight queries do not produce that standing.

saved_view.v1 carries producer_status: produced_elsewhere: the saved views themselves are recorded, by the saved-view operations rather than by the analytics projection, so they are read through those operations and not from an insight query.

cohort_projection.v1 also carries producer_status: produced_elsewhere. Its frozen comparison rule and qualified application counts are composed by the comparison results operation described above. Generic insight queries do not produce that estimate or grant access to comparison evidence.

Two spend figures on this product are computed over different populations and must not be subtracted from each other. A paired comparison’s measured sample reduction is arithmetic over the tested cases, on the baseline and candidate configurations, in the window the test ran. Provider spend here is settled cost on production traffic, over the window you asked about, on whatever release was deployed. Different window, different rows, different baseline. A reduction measured on a sample is not money that left your account, and neither figure becomes the other by subtraction.

Saving, editing and deleting a view are write operations this page leaves out, not ones the clients lack: cloud view save|edit|delete takes the question as a file, and both SDKs carry create, update and delete. Only the portal can save the question that is on a screen, because it is the surface that has one. App feedback is a different credential again: a runtime key at the gateway, so neither the CLI nor the customer-API client carries it. Charts belong to the portal, not to this API. And a drill-down never returns prompt or completion text: the tables behind it hold none.

Partial counts and additive spend/token sums are lower bounds. Ratios (including cost per request) and percentiles computed from incomplete populations can move higher or lower as missing rows arrive; they are not lower bounds. A producer with no indexed observation has unmeasured freshness, even if both reported counters are zero.