# Serving telemetry

> Reference for GET /v1/telemetry/stream and GET /v1/metrics/stream — server-sent event streams that report how each request was queued, routed, cached, timed and priced, and fleet-wide counters.

Mortar reports what happened to each request on a separate event stream, so the chat-completions response stays exactly OpenAI-shaped. Subscribe before you send a request and you'll see its routing decision, prefix-cache outcome, latency and cost as they happen.

Both streams use named events: each frame has an `event:` line naming it and a `data:` line holding a JSON object. A browser `EventSource` can listen for each name separately.

## Stream serving telemetry

`GET /v1/telemetry/stream`

Follows the requests in one of your organization's sessions. Send your API key to see them: the stream carries events only from requests made by the same organization in the same session.

A key is optional, so a browser can open the stream. Without one, the stream still opens, but it belongs to that connection alone and no request can publish to it, so it stays empty.

### Query parameters

| Parameter | Type | Description |
| --- | --- | --- |
| `session` | string | The session to follow: the `X-Mortar-Session` your chat completions send or receive. Up to 128 letters, digits, `-`, `_`, `.` or `:`. Leave it out and Mortar starts a session for this connection and reports it as `sessionId` in `stream.open`; send that as `X-Mortar-Session` on the requests you want to watch. |

> [!NOTE]
> Sessions belong to an organization. Two organizations that choose the same session ID never see each other's events, and no session ID reaches another organization's traffic. The stream carries token counts, timings and costs, never prompts or replies.

### Example

```bash title="Request"
curl -N "https://api.mortar.one/v1/telemetry/stream?session=3e7c9f1a-5b2d-4c8e-9a6f-0d1b2c3e4f5a" \
  -H "Authorization: Bearer $MORTAR_API_KEY"
```

```text title="Response, first event" output wrap
event: stream.open
data: {"sessionId":"3e7c9f1a-5b2d-4c8e-9a6f-0d1b2c3e4f5a","heartbeatMs":15000,"serverTime":1767225600000}
```

`stream.open` arrives at once. After it, Mortar sends one event for each step of every request your organization makes in the session. Every event after `stream.open` carries `at`, the time it was published in Unix milliseconds, and events about a completion carry its `requestId`. A `: ping` comment every 15 seconds keeps idle connections open.

### Events

| Event | When | Fields |
| --- | --- | --- |
| `stream.open` | You connect. | `sessionId`, `heartbeatMs`, `serverTime` |
| `request.queued` | A completion is accepted. | `promptTokens`, `queueDepth`, `queueWaitMs`, `contextLimit`, `maxTokens` |
| `routing.decision` | A replica is chosen. | `deployment`, `replicaId`, `model`, `modelId`, `gpu`, `gpuClass`, `environment`, `environmentId`, `byoc`, `engine`, `reason`, `reasonDetail`, `endpoint` |
| `cache.prefix.hit` | Part of the prompt was already in the replica's prefix cache. | `reusedTokens`, `prefilledTokens`, `reuseRatio`, `tier`, `tierLabel`, `fetchMs` |
| `cache.prefix.miss` | Nothing could be reused. | The same fields as `cache.prefix.hit` |
| `latency.ttft` | The first token is ready. | `ttftMs` |
| `prefill.complete` | The prompt has been processed. | `prefilledTokens`, `prefillMs` |
| `decode.progress` | Every few chunks while the reply streams. | `tokens`, `tokensPerSecond` |
| `cost.update` | The reply is finished and priced. | `promptTokens`, `completionTokens`, `totalTokens`, `billedPromptTokens`, `cacheReadTokens`, `cacheWriteTokens`, `cacheReadUsd`, `cacheWriteUsd`, `inputUsd`, `outputUsd`, `totalUsd`, `baseline`, `baselineUsd`, `savedUsd` |
| `session.metrics` | After every request. | `requests`, `promptTokens`, `completionTokens`, `totalTokens`, `reusedTokens`, `prefilledTokens`, `cacheReadTokens`, `cacheWriteTokens`, `cacheHitRate`, `actualUsd`, `baseline`, `baselineUsd`, `savedUsd`, `medianTtftMs` |
| `assistant.done` | The reply finished. | `completionTokens`, `finishReason` |
| `assistant.aborted` | The reply was stopped or failed. | `completionTokens`, `reason`, which is `cancelled` or `upstream_error` |
| `room.created` | A [conversation](/docs/api/conversations) was created with this session. | `room` |
| `room.updated` | A conversation was renamed or ended. | `room` |
| `room.deleted` | A conversation was deleted. | `roomId` |

### Reading cache and cost events

`reuseRatio` is the share of the prompt served from the prefix cache, from 0 to 1. `tier` says where the cached prefix was found: `gpu-hbm`, `host-memory` or `storage`, and `null` on a miss.

In `cost.update`, `cacheReadTokens` are the prompt tokens read from the prefix cache, priced at the model's `costPerMillionCacheRead`. `cacheWriteTokens` are the ones processed and written to the cache for the next request to read, priced at its `costPerMillionCacheWrite`. Both prices are in [`GET /v1/models`](/docs/api/models#list-models). `cacheReadUsd` and `cacheWriteUsd` are what each cost, and `inputUsd` includes both. `billedPromptTokens` equals `cacheWriteTokens` and stays for older clients.

`baseline` names what `baselineUsd` prices: `Same model, no prefix cache`, with every prompt token at the input price. `savedUsd` is `baselineUsd` minus `totalUsd`. A cache write costs more than an input token, so `savedUsd` is negative when the writes outweigh the reads, as on a cold first request, which reads nothing:

```json title="cost.update data for a cold first request" example
{
  "at": 1767225600000,
  "baseline": "Same model, no prefix cache",
  "baselineUsd": 0.0000441886,
  "billedPromptTokens": 502,
  "cacheReadTokens": 0,
  "cacheReadUsd": 0,
  "cacheWriteTokens": 502,
  "cacheWriteUsd": 0.00001964075,
  "completionTokens": 120,
  "inputUsd": 0.00001964075,
  "outputUsd": 0.000028476,
  "promptTokens": 502,
  "requestId": "chatcmpl-7c1e9a3f5b2d4c6e8a0f1b3d",
  "savedUsd": -0.00000392815,
  "totalTokens": 622,
  "totalUsd": 0.00004811675,
  "usageSource": "estimate"
}
```

`usageSource` says where the token counts came from. It's `upstream` when the deployment forwards to a model server that reported usage: the counts are that server's, and its `cached_tokens` are the cache reads. It's `estimate` when Mortar counted them itself: there's no model server, as for the request above, or the server reported none, or it ran on past `max_tokens` until Mortar ended the reply.

`session.metrics` adds up the session's requests the same way: its `savedUsd` is the sum of theirs, `reusedTokens` equals `cacheReadTokens`, and `prefilledTokens` is the prompt tokens not read from the cache, which equals `cacheWriteTokens` unless a model server reported fewer writes. Its `usageSource` is `upstream` or `estimate` when every request's was, and `mixed` otherwise. Amounts are US dollars at [sample pricing](/models), sent to 12 significant digits. The session adds up its requests' exact amounts and rounds only what it sends, so the rounding never accumulates.

## Stream fleet metrics

`GET /v1/metrics/stream`

Sends fleet-wide counters every 2 seconds, for dashboards such as the one on Mortar's home page. The figures are sample data that move within realistic bounds, not measurements of your traffic. No key is needed.

### Example

```bash title="Request"
curl -N https://api.mortar.one/v1/metrics/stream
```

```text title="Response, first event" output wrap
event: metrics.tick
data: {"requests":4820148,"tokensProcessed":1900081374,"medianTtftMs":210,"blendedCostPerMillion":1.53,"cacheHitRate":0.62,"requestsPerSecond":148,"activeReplicas":6,"at":1767225600000}
```

### Events

| Event | When | Fields |
| --- | --- | --- |
| `metrics.tick` | Every 2 seconds. | `requests`, `tokensProcessed`, `medianTtftMs`, `blendedCostPerMillion`, `cacheHitRate`, `requestsPerSecond`, `activeReplicas`, `at` |
