[Core][Frontend] Bound duplex output delivery and reject cancelled audio (#7644)

Signed-off-by: Nolen Liang <nliang@nvidia.com>
Signed-off-by: bcsdhjew <nliang@nvidia.com>
This commit is contained in:
bcsdhjew authored and GitHub committed 2026-10-04 14:06:04 -07:00
1 parent 3f20252151
commit f781f7bb08
26 files changed
+1170 -147

No files matched your search

+37 -1
View File
@@ -250,6 +250,40 @@ call; transport failures raise `DuplexConnectionError`; a session that ends
while you are awaiting it raises `DuplexSessionClosedError`. All three derive
from `DuplexClientError`.
### Output limits and slow consumers
Deployment settings under `duplex_session` bound each session's pending
output: `max_pending_output_bytes_per_session` defaults to 2 MiB and
`max_pending_output_events_per_session` to 512 events. Bytes count compact
Realtime JSON, including base64 audio, not decoded audio. The limits apply
to both WebSocket delivery and direct Python handles, not to replay journals
or client playback queues.
Exceeding a limit fails the active response without committing its partial
text to history, then closes only that session. If overflow occurs while a
completion is being queued, its not-yet-queued `response.done` becomes failed
with the same identity and output, and its response history entry is removed.
An ending already accepted by the buffer is not replaced or followed by a
second ending. Open a new session to continue; the closed session cannot be
resumed. The notifications use these fields:
| Event | Field values |
| --- | --- |
| `error` | `error.type: "rate_limit_error"`, `error.code: "output_backpressure"` |
| `response.done` (active response or not-yet-queued ending) | `response.status: "failed"`, `response.status_details: {"type": "failed", "reason": "output_backpressure"}` |
| `session.closed` | `reason: "output_backpressure"` |
Queued audio for the affected response is removed. Previously queued non-audio
events retain their order, but newly emitted ordinary events, including
audio/text, content-part and output-item completion markers, are suppressed.
Clients must therefore handle closure without waiting for every completion
marker. Errors and response endings have an extra 64 KiB / eight-event reserve;
if that also fills, either notification may be omitted. Closure has its own
independent slot. A closure notification exceeding 4 KiB is compacted to
`reason: "close_details_exceed_output_limit"` with empty details, preserving
the event type and identity. One event held by the consumer is outside the
queued budget; audio already sent still requires the client to stop playback.
### Measure latency
`EventCollector` accumulates events for assertions and metrics:
@@ -315,6 +349,8 @@ single-consumer async iterator that ends after `session.closed` /
`session.expired`; rejected commands come back as `ErrorEvent`s on it.
Every event renders the wire JSON with `to_realtime()`, so anything written
against the WebSocket protocol works unchanged on the typed stream.
If opening fails, the abandoned handle is closed. Late engine events do not
reopen a locally closed handle.
`vllm_omni.clients.inline_duplex.InlineDuplexClient` wraps a `DuplexOmni`
behind the `DuplexClient` API, so the same application code runs in-process
@@ -573,7 +609,7 @@ formats (`pcm16`, `pcm_s16le`, `s16le`, `pcm_f32le`, `g711_ulaw`,
| `session.created` | 2 | Session opened. OpenAI name; adds `attachment_generation`, `resume_token` and vLLM-Omni keys inside `session` (`id` is the server-allocated session id, `epoch`, `turn_id`, `playback`, `capabilities`, ...). |
| `session.updated` | 2 | Echo of the effective session config after every `session.update` (same extended `session` object). |
| `session.heartbeat_ack` | 3 | Reply to `session.heartbeat`. |
| `session.closed` | 3 | Last event on the socket, emitted once the engine released the session; `reason` ∈ `client_close`, `disconnect`, `timeout`, `transport_error`, `shutdown`, ... |
| `session.closed` | 3 | Last event on the socket, emitted once the engine released the session; `reason` ∈ `client_close`, `disconnect`, `timeout`, `transport_error`, `shutdown`, `output_backpressure`, `close_details_exceed_output_limit`, ... |
| `session.resumed` | 3 | Resume accepted; carries the new `attachment_generation` and rotated `resume_token`; journaled events are replayed after it. |
| `session.replaced` | 3 | Sent to the superseded socket when another socket resumes the session. |
| `session.expired` | 3 | Engine lease reaped (`disconnect_grace_expired`, `idle_ttl_expired`); socket closes after it. |