Commit acf361c
authored
fix(backend): name the serving provider on the live-STT ready event (#11822)
Closes the remaining backend slice of #11306.
## What was wrong
`ListenSessionRuntime.run` emitted a bare `ready` as soon as the
provider socket opened:
```python
self.send_event(MessageServiceStatusEvent(status='ready'))
```
`_create_stt_socket` can walk the fallback chain (#11695, #11752), so by
the time this
fires the session may be served by a provider other than the one the
serving policy
selected. The client clears its terminal-failure state on `ready`, so
"Listening" equally
described a healthy session and a fallback socket about to die, with
nothing in the
payload to tell them apart.
`MessageServiceStatusEvent` already carries `provider` and `reason` —
added as the
additive terminal-failure contract and unused on this path — and
`ListenReceiver._serving_provider()` already resolves the serving
provider at use time.
This wires the two together.
@kodjima33 named this slice explicitly when merging the attribution half
as #11359:
> Still open here: … surfacing the actual provider + fallback reason on
the `ready` event
> so the client can tell a fallback session from a healthy Parakeet one.
## What changed
- `ready` now carries `provider`, read from `_serving_provider()` **at
emission time**.
Resolving it any earlier is precisely the attribution bug #11359 fixed
on the
terminal-failure path, so the accessor is called here rather than
reusing a value held
from bootstrap.
- `_bootstrap` records the policy-**selected** provider
(`stt_service_selected`). That
value is fixed at selection and is not a snapshot of the serving
provider; it exists
only so the emission can tell whether the chain moved the session.
- When serving ≠ selected, `reason` is `fallback_from_<selected>`. A
healthy session
carries no `reason` at all.
- Custom-STT sessions claim no backend provider — the client produces
its own
transcripts — so neither field is emitted.
Payload for a Modulate session that Velma refused and Deepgram took
over:
```json
{"type": "service_status", "status": "ready", "provider": "deepgram", "reason": "fallback_from_modulate"}
```
## Scope
**Backend emission only.** Making the clients *act* on the new fields is
a separate,
larger change across three platforms (Flutter app, iOS, desktop) and is
deliberately not
included here. This PR only makes the information available to them.
The underlying vendor cause of a fallback (`quota` / `timeout` /
`provider_5xx`) is
classified inside `connect_stt_socket_with_fallback` and already
recorded through
`record_fallback`; it is not returned to the caller. Plumbing it onto
the event would
change that shared helper's signature and its four call sites, so it is
left out of this
slice. No new provider-changing or mode-changing branch is introduced
here, so the
fallback-telemetry contract needs no new `record_fallback` call.
## Compatibility
Purely additive. `MessageServiceStatusEvent.to_json` uses
`exclude_none=True`, so a client
that does not read the new fields sees exactly the payload it sees today
— verified by
`test_ready_stays_additive_for_clients_that_ignore_the_new_fields`.
This is a WebSocket event and does not appear in the REST app-client
contract
(`grep -c service_status docs/api-reference/app-client-openapi.json` →
`0`). The gate was
run anyway:
```
$ python scripts/check_app_client_openapi_compatibility.py --base-ref upstream/main
App-client OpenAPI compatibility passed against merge-base 5712bfa.
```
## Verification
### Red before green
The regression test was written first and run against unmodified code.
It drives the real
`run()` sequence with a real `ListenReceiver`, stubbing only the vendor
connect functions,
so the fallback it reports is produced by the production chain rather
than asserted about.
```
$ python -m pytest tests/unit/test_listen_ready_provider.py -q # before the fix
___________ test_ready_names_the_fallback_provider_actually_serving ____________
> assert payload['provider'] == 'deepgram'
E KeyError: 'provider'
_____ test_ready_on_a_healthy_selected_provider_carries_no_fallback_reason _____
> assert payload['provider'] == 'modulate'
E KeyError: 'provider'
______ test_ready_provider_is_resolved_at_emission_time_not_at_selection _______
> assert runtime._ready_event().to_json()['provider'] == 'parakeet'
E AttributeError: 'ListenSessionRuntime' object has no attribute '_ready_event'
_____ test_bootstrap_records_the_selected_provider_for_fallback_comparison _____
> assert runtime.stt_service_selected == STTService.modulate
E AttributeError: 'ListenSessionRuntime' object has no attribute 'stt_service_selected'
4 failed, 2 passed, 9 warnings in 2.72s
```
`KeyError: 'provider'` is the exact pre-fix shape: the field was `None`
and `exclude_none=True`
stripped it from the payload entirely. The first failure only reaches
its assertion after
`assert runtime.stt_service == STTService.deepgram` passes, which proves
the fallback chain
really moved the session before `ready` was emitted.
After the fix:
```
$ python -m pytest tests/unit/test_listen_ready_provider.py -q
6 passed, 9 warnings in 1.94s
```
### Blast radius
The full GitHub Actions unit contract for this diff —
`scripts/select_backend_unit_tests.py`
selected 148 files from the changed paths — run through `test.sh` with
CI's file-isolation
and timing guards:
```
$ BACKEND_UNIT_TEST_FILE_LIST=<selected> BACKEND_FAST_UNIT_FAIL_SECONDS=1.0 \
BACKEND_PYTEST_FILE_ISOLATION=1 bash test.sh
143 files, 2473 tests passed, exit 0
```
Type check and the isolation scanners:
```
$ bash scripts/typecheck.sh
0 errors, 3660 warnings, 0 informations
$ python scripts/check_module_stub_pollution.py
Checked 903 backend test file(s); 0 violation(s).
$ python scripts/scan_import_time_side_effects.py
Checked 800 backend production file(s); 0 violation(s).
$ black --line-length 120 --skip-string-normalization --check
2 files would be left unchanged.
```
### Not verified here
Four of the 148 selected files could not run on this machine, for
reasons unrelated to
this diff:
- `test_verify_pusher_config_references.py` shells out to `helm`, which
is not installed
(`FileNotFoundError: [Errno 2] No such file or directory: 'helm'`).
Excluded from the
2473-test run above; CI has helm.
- `test-preflight.sh` reports one failure: Python 3.11.13 installed vs
3.11.15 pinned in
`.python-version`. Patch-level, environmental.
**This was verified hermetically at the `process_audio_*` seam. It was
not exercised
against live Deepgram, Modulate, or Parakeet**, and I have not run a
real device session
against it. The provider values asserted are the ones the production
fallback chain
assigns to `host.stt_service`; that those match what a live vendor
socket serves is
existing behavior this PR does not change.
## Gates
- **Product invariants affected:** none (`scripts/pr-preflight --base
upstream/main --suggest`).
- **Docs:** `docs/doc/developer/backend/transcription.mdx` and
`listen_pusher_pipeline.mdx` both documented the `ready` payload and are
updated.
Failure-Class: none
Rationale: this adds reporting fields to an existing event. It
introduces no new
failure, recovery, or fallback branch — the fallback behavior it reports
on landed in
#11695/#11752/#11814. #11359, the attribution half of this same issue on
the same code
path, also declared `Failure-Class: none`. `scripts/failure-class
prepare` inferred no
class from the diff.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/BasedHardware/omi/pull/11822?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->4 files changed
Lines changed: 327 additions & 4 deletions
File tree
- backend
- routers/listen
- tests/unit
- docs/doc/developer/backend
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
115 | 115 | | |
116 | 116 | | |
117 | 117 | | |
| 118 | + | |
118 | 119 | | |
119 | 120 | | |
120 | 121 | | |
| |||
275 | 276 | | |
276 | 277 | | |
277 | 278 | | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
278 | 283 | | |
279 | 284 | | |
280 | 285 | | |
| |||
574 | 579 | | |
575 | 580 | | |
576 | 581 | | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
577 | 608 | | |
578 | 609 | | |
579 | 610 | | |
| |||
628 | 659 | | |
629 | 660 | | |
630 | 661 | | |
631 | | - | |
| 662 | + | |
632 | 663 | | |
633 | 664 | | |
634 | 665 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
139 | 139 | | |
140 | 140 | | |
141 | 141 | | |
142 | | - | |
| 142 | + | |
143 | 143 | | |
144 | 144 | | |
145 | 145 | | |
| |||
478 | 478 | | |
479 | 479 | | |
480 | 480 | | |
481 | | - | |
| 481 | + | |
482 | 482 | | |
483 | 483 | | |
484 | 484 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
563 | 563 | | |
564 | 564 | | |
565 | 565 | | |
566 | | - | |
| 566 | + | |
| 567 | + | |
567 | 568 | | |
568 | 569 | | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
569 | 577 | | |
570 | 578 | | |
571 | 579 | | |
| |||
0 commit comments