Skip to content

feat: expose provider request ids on STT/TTS/LLM spans for debugging#5546

Open
longcw wants to merge 4 commits intomainfrom
longc/trace-provider-context-ids
Open

feat: expose provider request ids on STT/TTS/LLM spans for debugging#5546
longcw wants to merge 4 commits intomainfrom
longc/trace-provider-context-ids

Conversation

@longcw
Copy link
Copy Markdown
Contributor

@longcw longcw commented Apr 24, 2026

Adds a new span attribute lk.provider_request_ids (list[str], deduped) on the user_turn (STT), tts_request_run (TTS), and llm_request_run (LLM) spans for debugging/reporting STT or TTS issues to the provider.

Supported plugins (id actually known to the provider)

For LLM, the id comes from ChatChunk.id.

TTS streaming (tts_request_run attribute populated)

  • asyncai — context_id echoed back
  • cartesia — context_id echoed back
  • elevenlabs — context_id sent on every WS message
  • inworld — server-returned contextId
  • minimax — server-returned session_id
  • murf — context_id echoed back
  • neuphonic — context_id sent in every message
  • sarvam (updated in this PR) — server request_id via note_provider_request_id()
  • upliftai (updated in this PR) — reuses the outbound requestId as segment_id
  • inference (LiveKit gateway) — server-returned session_id

STT (user_turn attribute populated when present)

  • deepgrammetadata["request_id"]
  • google — from response metadata
  • gladia — session id from init response
  • mistralaisession.request_id
  • cartesia — server request_id (falls back to local UUID if absent)
  • sarvam — per-message request_id (with fallback to session-wide server id; fallback added in this PR)
  • openai realtime (updated in this PR) — item_id from completion event
  • inference (LiveKit gateway) — server request_id (present when gateway includes it)

longcw added 4 commits April 24, 2026 17:22
Adds a new span attribute 'lk.provider_context_ids' (list[str]) populated
from plugin-provided STT request_ids and TTS segment_ids, so users can
cross-reference traces with provider-side logs when debugging.

- TTS: AudioEmitter.start_segment appends non-empty segment_ids to a
  deduped list and sets the attribute on the current tts_request_run span.
- STT: AudioRecognition collects SpeechEvent.request_ids across a turn
  and writes the full list on the user_turn span at span end.
- tts base: add AudioEmitter.note_provider_context_id() for plugins where
  the provider id arrives after start_segment() has been called.
- sarvam TTS: feed the captured server request_id via note_provider_context_id
  once the first WS response arrives.
- sarvam STT: fall back to the session-wide server request_id when a
  per-message request_id is missing.
- openai STT (realtime): populate SpeechEvent.request_id from item_id;
  track current_item_id for interim transcripts.
- upliftai TTS: pass segment_id as the request id to client.synthesize()
  so the id is actually known to the server.
LLMStream now tracks the deduped list of ChatChunk.id values seen during
each attempt and writes them to the llm_request_run span as
lk.provider_context_ids, matching the per-attempt scope used for TTS.

The list is reset in _main_task at the start of each attempt, populated
by the monitor task as chunks arrive, and read at the end of the attempt
(before the span closes). No plugin changes needed — ChatChunk.id is
already the provider's response id across all LLM plugins.
Now that the attribute covers STT (request_id), LLM (ChatChunk.id, also a
request/response id), and TTS (context_id / session_id), 'context_ids' was
only accurate for TTS. 'request_ids' matches the terminology STT and LLM
providers actually use and reads naturally for TTS context_ids too.

Renames:
- ATTR_PROVIDER_CONTEXT_IDS -> ATTR_PROVIDER_REQUEST_IDS
- lk.provider_context_ids -> lk.provider_request_ids
- AudioEmitter.note_provider_context_id() -> AudioEmitter.note_provider_request_id()
- _provider_context_ids / _stt_context_ids -> _provider_request_ids / _stt_request_ids
@chenghao-mou chenghao-mou requested a review from a team April 24, 2026 10:28
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment on lines +684 to +685
if ev.request_id and ev.request_id not in self._stt_request_ids:
self._stt_request_ids.append(ev.request_id)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Sarvam STT's JSON-encoded RECOGNITION_USAGE request_id pollutes provider_request_ids span attribute

The new _on_stt_event collection code in audio_recognition.py:684 indiscriminately collects ev.request_id from all event types. The Sarvam STT plugin emits RECOGNITION_USAGE events with request_id=json.dumps(request_data) at stt.py:1334, where request_data is a dict containing original_id and processing_latency. This JSON string (e.g. {"original_id": "abc", "processing_latency": 0.5}) gets appended to _stt_request_ids and eventually written to the user_turn span as lk.provider_request_ids. This pollutes the provider request IDs attribute with non-ID metadata strings, undermining the debugging purpose of the feature.

Example of polluted span attribute

The span's lk.provider_request_ids would contain entries like:
["actual-server-request-id", "{\"original_id\": \"actual-server-request-id\", \"processing_latency\": 0.123}"]

instead of just: ["actual-server-request-id"]

Prompt for agents
The _on_stt_event method collects ev.request_id from ALL SpeechEvent types into _stt_request_ids. However, the Sarvam STT plugin (livekit-plugins/livekit-plugins-sarvam/livekit/plugins/sarvam/stt.py, around line 1334) sets request_id on RECOGNITION_USAGE events to json.dumps(request_data) which includes processing_latency metadata - not a clean provider ID. This JSON string gets collected alongside actual request IDs and written to the span.

Two possible fixes:
1. In audio_recognition.py _on_stt_event, skip collecting request_id from RECOGNITION_USAGE events (e.g. add a check: if ev.type != stt.SpeechEventType.RECOGNITION_USAGE).
2. In the Sarvam STT plugin, stop encoding metadata into the request_id field of RECOGNITION_USAGE events and instead use just the plain request_id string.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +227 to +231
finally:
if self._provider_request_ids:
attempt_span.set_attribute(
trace_types.ATTR_PROVIDER_REQUEST_IDS, self._provider_request_ids
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 LLM _provider_request_ids may be incomplete when set on attempt span due to race with monitor task

In llm.py, _provider_request_ids is populated by the _metrics_monitor_task (line 294-295) which runs as a separate asyncio task, and read in the finally block of _main_task (line 228-231). When _run() returns, the finally block executes synchronously before the event loop can schedule the monitor task. Any ChatChunk events that were send_nowait'd by _run() after its last internal await but before returning will be buffered in the tee iterator and not yet processed by the monitor task. This means the attempt_span's lk.provider_request_ids attribute could be incomplete. In practice, since LLM request IDs are typically identical across all chunks, the first processed chunk captures the ID, making this unlikely to cause missing data — but it's architecturally fragile.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

self.note_provider_request_id(segment_id)
return self.__start_segment(segment_id=segment_id)

def note_provider_request_id(self, context_id: str) -> None:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def note_provider_request_id(self, context_id: str) -> None:
def _note_provider_request_id(self, context_id: str) -> None:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants