Skip to content

Only send modern transcriptions to client sdks which can auto back convert modern -> legacy transcriptions internally - #2526

Open
1egoman wants to merge 6 commits into
mainfrom
port-transcription-dedup
Open

1egoman wants to merge 6 commits into
mainfrom
port-transcription-dedup

Conversation

@1egoman

@1egoman 1egoman commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Description

The agents-js port of livekit/agents#7240. A high level summary of the transcription deduplication project:

Today all transcriptions are sent twice over the reliable data channel, once in legacy format, and once in modern data streams format. Because they are sent twice, the reliable data channel is clogged with data and when an end user is on a poor bandwidth network connection (ie, mobile) this can result in a very poor user experience - RPCs get missed, data stream are only partially delivered, etc.

In livekit/client-sdk-js#2093, I added a new client protocol of 3. Any client which advertises this new client protocol version ignores all legacy transcriptions, and back-converts modern transcriptions into in-memory legacy transcriptions.

This pull request implements the converse of this behavior on the agent end: if any connected client sdk advertises support for the client protocol of 3, then the agents sdk now will skip sending legacy transcriptions to all participants.

A few important caveats:

  • Note the phrase "all participants" here, rather than just "one participant" - this was purposeful because filtering by setting destination_identities wouldn't only deliver the packet to the listed participants.
    • Reason behind this change: packet destination identity filtering happens client sdk side, so it's impossible today to deliver a packet to only a subset of participants.
    • This does mean this will take slightly longer to roll out (because all clients in a room will need to be updated for this to be enabled) but I think this should happen in practice in fairly short order as users update their sdks across all platforms, especially for those that are performance concious
  • This "all participants" list intentionally excludes a few types of participants, namely SIP, ingress, and egress. I've looked through the sip, ingress, and egress codebases and none use legacy (or modern for that matter) transcriptions.
    • Reason behind this change: The go sdk still advertises a client protocol of 0, so without this special case any room with (for example) ingress or egress enabled would not pass this check. We're working on changing this in the go sdk but it's a longer lead project (in concert with core services) which shouldn't be expected to be done anytime soon.
    • This wouldn't block sip, ingress, or egress from using transcriptions in the future, and if they were going to use transcriptions, they'd want to consume modern transcriptions anyway.

Pre-Review Checklist

  • Build passes: All builds (lint, typecheck, tests) pass locally
  • AI-generated code reviewed: Removed unnecessary comments and ensured code quality
  • Changes explained: All changes are properly documented and justified above
  • Scope appropriate: All changes relate to the PR title, or explanations provided for why they're included
  • Video demo: A small video demo showing changes works as expected and did not break any existing functionality using Agent Playground (if applicable)

Testing

  • Automated tests added/updated (if applicable)
  • All tests pass
  • Make sure both restaurant_agent.ts and realtime_agent.ts work properly (for major changes)

In addition to newly added unit tests, I have tested this end to end locally - I have run the client-sdk-js both advertising client protocol of 2 and client protocol of 3:

  • When advertising client protocol of 2: Many data packets are received which match packet.value.case === "transcription", both legacy and modern transcription events emitted
  • When advertising client protocol of 3: NO transcription data packets were received, both legacy and modern transcription events emitted

Note to reviewers: Please ensure the pre-review checklist is completed before starting your review.

@changeset-bot

changeset-bot Bot commented Sep 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: db798ea

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 39 packages
Name Type
@livekit/agents Patch
@livekit/agents-plugin-anam Patch
@livekit/agents-plugin-anthropic Patch
@livekit/agents-plugin-assemblyai Patch
@livekit/agents-plugin-azure Patch
@livekit/agents-plugin-baseten Patch
@livekit/agents-plugin-bey Patch
@livekit/agents-plugin-cartesia Patch
@livekit/agents-plugin-cerebras Patch
@livekit/agents-plugin-deepgram Patch
@livekit/agents-plugin-did Patch
@livekit/agents-plugin-elevenlabs Patch
@livekit/agents-plugin-fishaudio Patch
@livekit/agents-plugin-google Patch
@livekit/agents-plugin-hume Patch
@livekit/agents-plugin-inworld Patch
@livekit/agents-plugin-krisp Patch
@livekit/agents-plugin-lemonslice Patch
@livekit/agents-plugin-liveavatar Patch
@livekit/agents-plugin-livekit Patch
@livekit/agents-plugin-meta Patch
@livekit/agents-plugin-minimax Patch
@livekit/agents-plugin-mistral Patch
@livekit/agents-plugin-mistralai Patch
@livekit/agents-plugin-neuphonic Patch
@livekit/agents-plugin-openai Patch
@livekit/agents-plugin-perplexity Patch
@livekit/agents-plugin-phonic Patch
@livekit/agents-plugin-protoface Patch
@livekit/agents-plugin-resemble Patch
@livekit/agents-plugin-rime Patch
@livekit/agents-plugin-runway Patch
@livekit/agents-plugin-sarvam Patch
@livekit/agents-plugin-silero Patch
@livekit/agents-plugin-soniox Patch
@livekit/agents-plugin-tavus Patch
@livekit/agents-plugins-test Patch
@livekit/agents-plugin-trugen Patch
@livekit/agents-plugin-xai Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@1egoman 1egoman changed the title Only send modern transcriptions to client sdks which can auto back convert modern -> legacy transcriptions internally- #7240 Only send modern transcriptions to client sdks which can auto back convert modern -> legacy transcriptions internally Sep 18, 2026
The agent sends each transcription two times. It sends a deprecated `Transcription`
data packet. It also sends an `lk.transcription` text stream. The two copies fill the
reliable data channel. Other reliable traffic becomes slow.

This commit stops the legacy packet when no client needs it. Before each legacy
publish, the agent examines the remote participants. If all applicable participants
have client protocol 3 or higher, the agent does not send the packet. The agent
always sends the text stream.

Only STANDARD participants are applicable. These are the client SDK instances that
users create. SIP, INGRESS, AGENT, CONNECTOR and BRIDGE participants do not show
legacy transcripts. EGRESS participants are hidden and do not appear in the
participant list. The agent also ignores its own avatar worker.

An absent client protocol counts as 0, which means a legacy client. This decision is
necessary, not defensive. protobuf-es gives the field the optional type, and in
JavaScript all comparisons with `undefined` are false. Without the coalesce, a missing
field reads as a modern client, and the agent stops the transcripts of a client that
still needs them.

The avatar worker test compares two values only when the attribute is present. An
absent attribute and an absent local identity are both `undefined`. A direct
comparison is true for each participant that has no such attribute.

The gate is in `publishTranscription`. This is the only function that sends the
packet. Both `handleCaptureText` and `handleFlush` use this function.

Do not move the gate into `handleCaptureText`. Each legacy packet contains the full
segment text with a stable identifier. The accumulated text must stay correct. If a
legacy client joins during a segment, the next packet gives it the full segment.

The agent calculates the result again for each publish. Do not put the result in a
cache. `legacyStatusLogged` holds only the last status written to the log. It prevents
a log message for each transcription chunk.

This is a port of livekit/agents#7240 to node.
This commit adds five test helpers. It adds no tests.

`createFakeRoom` makes a room stand-in. `isConnected` is true. If it were false,
`publishTranscription` would never reach the local participant, and each test would
pass for an incorrect reason.

`fakeRemote` makes a stand-in for a remote participant. The `kind` value is always
set. The real getter gives STANDARD when the field is absent, so a fake without a
kind would not test its own condition.

`makeLegacyOutput` makes a legacy sink with `Object.create` and sets the private
fields directly. This prevents the constructor, which attaches listeners to a real
room.

`captureAndFlush` sends text through the sink and then waits for the flush task.
`publishedSegments` collects the segments from all publish calls.
This commit adds three tests for the client protocol comparison.

The first test shows that the agent sends no legacy packet when all clients have
client protocol 3.

The second test shows that the agent sends the legacy packets when one client is
older. The test also examines the packets. The partial packet and the final packet
must have the same segment identifier and the full text.

The third test shows that an absent client protocol counts as a legacy client. This
test has no equivalent in the Python code, because the JavaScript behavior is
different. protobuf-es gives the field the optional type, and `undefined < 3` is
false. Without the coalesce in the gate, this participant reads as a modern client
and loses its transcripts. The failure is silent.
This commit adds nine tests. These tests hold the rules about applicable
participants. Change these rules only with care.

Two conditions show that the agent sends no legacy packet when the room has no
STANDARD participant. An empty room and a room with only a SIP participant give the
same result. A SIP participant does not show transcripts.

Four conditions show that SIP, INGRESS, AGENT and CONNECTOR participants do not
control the result. A further test shows that a STANDARD participant with an old
client protocol does control the result.

Two tests show the avatar worker rules. The agent ignores its own avatar worker. The
agent does not ignore the avatar worker of a different agent.

The last test has no equivalent in the Python code. It sets the local identity to
`undefined`. An absent attribute and an absent local identity are both `undefined`.
A direct comparison of the two is true, and the agent then ignores each participant
that carries no publish-on-behalf attribute. The room becomes empty for the gate, and
all legacy transcripts stop.
This commit adds two tests.

The first test shows that the gate obeys changes in the room. All clients are modern
at the start, and the agent sends no packet for the first text. A legacy client then
joins. The agent sends one packet for the second text. This packet contains the full
text of both parts, not only the second part. The final packet has the same segment
identifier. This test fails if a person moves the gate into `handleCaptureText`.

The second test shows that the `lk.transcription` text stream does not change. The
stream always goes to all participants, also when the agent sends no legacy packet.
@1egoman
1egoman force-pushed the port-transcription-dedup branch from e68538e to db798ea Compare September 18, 2026 16:33
@1egoman
1egoman marked this pull request as ready for review September 18, 2026 16:39
@1egoman
1egoman requested a review from a team as a code owner September 18, 2026 16:39

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

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.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Devin Review

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.

1 participant