Skip to content

accelerate-v0.6.311

@kanat kanat tagged this 08 Nov 16:52
Linear: [AI-2879](https://linear.app/stream/issue/AI-2879), router
launch metrics (under AI-2588).

**Ready for review. Not merged: Kanat merges after review.**

## What it does

The router's `/metrics` now serves the signals that the launch alerts
need. Every label is bounded: no customer, app, session, call or key
value.

```
/metrics
├── API requests      count and latency per route template          #1786  AI-2778
├── Sessions          active, ends by reason / fault / stage         #1782  AI-2779, #1796 AI-2831, #1839 AI-2949
├── Failover          LLM / STT / TTS vendor moves per kind          #1785  AI-2781
├── Turns             turn latency, start-up stages and failures     #1780  AI-2826, #1794 AI-2832, #1841 AI-2969
├── Silent calls      caller sent no audio, per call kind            #1792  AI-2828
├── Phone             call outcomes per vendor and direction,        #1783  AI-2780
│                     carrier KPIs                                   #1793  AI-2830
└── Rate limits       vendor 429 refusals per key and key headroom   #1781  AI-2833
```

A failed model call now says why, and no secret leaks on the way:

```
provider error ──► model_call frame  error_message (start of the vendor's text)   #1834 AI-2954
                   failure log line  provider + model
          │
          └─► one redactor (store.RedactLogText / SafeLogText)                   #1836 AI-2958 AI-2959
                ├── client error frames and socket close reasons
                ├── every binary's slog handler (ReplaceAttr)                    #1838 AI-2966
                ├── Sentry events, contexts and tags                             #1838, #1846 AI-2975
                └── stored tool, recording and simulation errors                 #1846 AI-2975
```

A lost voice no longer loses the answer:

```
TTS vendor refuses mid-turn ──► next voice says again what the caller did not hear   #1840 AI-2970
                                held back- check-in kept, dropped if the caller talks   #1843 AI-2973, #1850 AI-2977
                                lost voice closes in the background (no 2 s wait)      #1844 AI-2974
```

Staging fixes (umbrella AI-2929):

| PR | Ticket | Fix |
|---|---|---|
| #1805 | AI-2930 | the rate-limit refusal metric is served at 0 for
each pooled key before any 429 |
| #1811 | AI-2939 | a failed reply marks the turn lost, even when the
flow-controller call succeeds |
| #1810 | AI-2940 | a table vendor with no key is served one
`key="none"` series at 0 |
| #1839 | AI-2949 | a create on an app with Video off counts as
`customer_config`, not the router's fault |
| #1840 | AI-2970 | after a TTS failover the next voice says the kept
answer again |
| #1841 | AI-2969 | turn latency counts delegated and tool answers;
scheduled turns are left out |
| #1843 | AI-2973 | a `back-` check-in is held across a voice loss |
| #1844 | AI-2974 | the lost voice closes in the background, so the next
voice starts at once |
| #1846 | AI-2975 | Sentry contexts and tags, and stored tool errors, go
through the redactor |
| #1850 | AI-2977 | test: the agent's own echo does not drop a held
check-in |

## Acceptance criteria (staging, last window on feature tip `7614abd2d`)

| AC | What | Result |
|---|---|---|
| AC-1 | request count and duration per route template, no config id in
labels | pass |
| AC-2 | active sessions go up and back; one session end with its
documented reason | pass |
| AC-3 | a refused customer LLM key ends with fault `customer_config`,
not a vendor failover | pass (after #1811) |
| AC-4 | a bad first LLM candidate counts one failover; a working one
counts none | pass (local router) |
| AC-5 | 3 turns add 3 turn-latency observations; each start-up stage
adds 1 | pass (rechecked on `7614abd2d`) |
| AC-6 | 60 s of caller silence counts one silent web call | pass
(rechecked on `7614abd2d`) |
| AC-7 | phone outcomes and carrier KPIs on an answered and an
unanswered call | **not checked**: no staging number on the test app,
and phonecheck runs only in the main session |
| AC-8 | header vendor headroom gauge, plus a refusal series for each
vendor in the table | pass (after #1805, #1810) |
| AC-9 | every existing `router_*` name is still served; no id-like
labels | pass |
| AC-10 | a refused customer OpenAI key: `model_call` frame
`success:false`, `error_message` holds the start of OpenAI's text, the
key is in no frame or log | pass (rechecked on `7614abd2d`) |
| AC-11 | a working model: `model_call` frame `success:true`, no
`error_message` key | pass |
| AC-12 | an upstream error that echoes `Authorization: Bearer …`:
`error_message` keeps the text, not the token | pass (local router) |
| AC-13 | the same failure: the router log line names provider and model
| pass (local router) |

Last window, also checked live:
- **Voice loss (AI-2974):** the time from the lost voice to the next
voice's first audio went from 2,157 ms to 120–158 ms (3 runs).
- **Check-ins:** a held check-in is said once on the next voice
(AI-2973).
- **Sentry:** no new issue, and no `sk-` or `Bearer` text in new events.

Evidence for each AC (commands, metric deltas, timestamps) is in the
verifier's comments on AI-2879.

## Gates for the merge into accelerate

None needs a flag. Migrations: none.

1. **Session API (behaviour change, AI-2831).**
   - `Session` gains `end_fault` and `end_stage`.
- `SessionEndReason` gains `closed`, `shutdown`, `agent_failed`,
`model_failed`, `model_config_refused` and `model_no_credit`.
- Sessions stopped through the API, ended by a shutdown, or failed now
carry an `end_reason`.
- Python and Rust are open enums since #1828 (AI-2951). Neither package
is on a registry. Only consumers pinned to an older git commit fail.
2. **Stored values.**
- `agent_sessions.end_reason` (TEXT) now holds the 6 new values. There
is no migration.
- A router rollback is safe on the server. It does not rewrite those
rows.
3. **Redis.**
   - A refused arriving call writes one SET with an expiry.
   - Every inbound leg end reads one EXISTS, with a 1 s timeout.
4. **`model_call.error_message`** on the frame and on the call
timeline's `model_calls`: an optional field that only adds. No client
release.
5. **New end reasons** `create_no_video`, `create_no_call_type` and
`create_no_channel_type`: these are metric labels only. Video-off
creates move from the router's fault to `customer_config`.
6. **Redacted errors.**
- Client-visible error frames, socket close reasons and stored tool
errors now go through the redactor.
   - A text over 8 KiB is cut to 2,048 runes with `… [truncated]`.
   - Rows already stored are not rewritten.
7. **CLI output.** `cmd/agent`, `chat`, `say` and `knowledge` now print
their final error as a logfmt `level=ERROR` line. The exit code is still
1.
8. **Re-said text after a TTS failover.**
- The next voice says again a reply, greeting, check-in, update or hold
line the caller did not hear (#1840, #1843). This adds TTS billing for
that text.
- The lost voice closes in the background (#1844), and stopping the
agent still waits for it.
9. **Turn event timing.** The session socket `turn` event of a turn that
only delegated or called a tool now comes at the first line said for it,
with a non-zero `roundtrip_ms` (#1841). A turn that a scheduled task
starts is no longer observed.

## Series

Upper bounds per router process:

| Area | Series |
|---|---|
| HTTP | about 20.2k, only for routes that were called |
| Sessions, silent calls, start-up, turn latency, failovers, phone |
1,155 together |
| Rate limits | one per pooled key, up to 11 `key="none"`, and headroom
of at most 5 per OpenAI key, 4 per Anthropic key and 1 per ElevenLabs
key |

Measured on staging: 338 and 428 new series per pod.

## Rollout

1. Merge into accelerate. Deploy to staging, then prod as usual.
2. The Datadog monitors and dashboards built on these metrics are
separate work (AI-2588). The provider balance exporter is live
separately (AI-2953, GetStream/chat).

## Open

- AC-7 needs a phone check on staging.
- Staging's ElevenLabs account refuses library voices through the API,
so the voice ACs ran on Cartesia. Every voice-loss check used that
refusal as its failover.
- Voice ACs ran on a test app with Video on. Rerun them on the main text
test app once its Video is on.
- Follow-up tickets: AI-2943, AI-2944, AI-2950, AI-2971, AI-2976.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Kanat <kanat.kiialbaev@getstream.io>
Assets 2
Loading