Skip to content

feat(inference): let gateway mint avatar token - #2152

Open
rosetta-livekit-bot[bot] wants to merge 1 commit into
mainfrom
fuddling-insolent-regicide
Open

feat(inference): let gateway mint avatar token#2152
rosetta-livekit-bot[bot] wants to merge 1 commit into
mainfrom
fuddling-insolent-regicide

Conversation

@rosetta-livekit-bot

@rosetta-livekit-bot rosetta-livekit-bot Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Ports livekit/agents#6575 so the inference avatar gateway mints the avatar worker room token instead of the JS SDK signing it locally.

Changes

  • Remove local avatar worker token minting from AvatarSession.start().
  • Send gateway mint inputs (room_name, avatar_identity, avatar_name, agent_identity) and omit livekit_token from avatar session create payloads.
  • Require only livekitUrl for start-time gateway session creation.
  • Add a patch changeset for @livekit/agents.
Source diff coverage
Source file Classification Target file Notes
livekit-agents/livekit/agents/inference/avatar.py Adapted agents/src/inference/avatar.ts Removed local AccessToken worker-token signing and livekit_token payload field; added avatar_name; kept room_name / avatar_identity / agent_identity as gateway mint inputs. JS-specific adaptation: livekitApiKey / livekitApiSecret start options remain only for the existing @livekit/rtc-node RoomServiceClient room-SID fallback when room.getSid() is empty; they no longer mint or sign the worker token.
tests/test_inference_avatar.py Adapted agents/src/inference/avatar.test.ts Ported the changed assertions to Vitest/TypeScript: no JWT decode/token-claim assertion, assert no livekit_token, assert gateway mint inputs including avatar_name, remove unnecessary room-project credentials from starts, and add the missing-livekitUrl failure coverage.

No source diff files were skipped.

Verification

  • pnpm test agents passed: 111 files, 1504 tests passed, 5 skipped.
  • pnpm --filter @livekit/agents build passed.
  • pnpm --filter @livekit/agents lint completed with existing warnings.
  • pnpm build passed.
  • pnpm lint completed with existing warnings.
  • git diff --check passed.

Additional non-required check: pnpm --filter @livekit/agents api:check fails before evaluating this change because API Extractor 7.43.7 does not support the existing export * as ___ syntax in agents/dist/index.d.ts:10.


Ported from livekit/agents#6575

Original PR description

What

inference.AvatarSession minted the avatar worker's room token locally and posted it to the gateway as livekit_token. The gateway now mints that token itself — livekit/agent-gateway#1095 — so this PR stops minting client-side and sends the fields the gateway mints from.

These two PRs must land together. Against gateway #1095, a create that sends livekit_token and omits agent_identity is a 400. No live impact either way: the gateway endpoint is gated by the default-off avatar_lemonslice flag, so no project can be provisioning avatars today.

Why

Billing integrity. Server-side avatar-minutes metering keys on the lk.avatar_provider participant attribute — and that attribute was being set by this code, in a token we signed. You cannot reconcile revenue against a value the customer's process controls. Same for room_name / room_sid / avatar_identity: self-reported body fields with nothing tying them to the token actually used.

Moving the mint server-side makes all of it gateway-authored. The gateway signs with the key it just authenticated the request on, so it grants nothing the caller could not already mint for itself.

The change

-        worker_token = (
-            api.AccessToken(api_key=lk_key, api_secret=lk_secret)
-            .with_kind("agent")
-            .with_identity(self._avatar_participant_identity)
-            .with_grants(api.VideoGrants(room_join=True, room=room.name))
-            .with_attributes({...})
-            .to_jwt()
-        )

The gateway reproduces exactly that grant set: kind=agent, the identity, roomJoin scoped to the one room, publish/subscribe left implicit, plus lk.publish_on_behalf and lk.avatar_provider.

Payload changes: livekit_token removed; avatar_name added so a custom avatar_participant_name still reaches the minted token. room_name / avatar_identity / agent_identity were already sent and are now load-bearing mint inputs rather than attribution.

⚠️ Breaking change (in a released API)

AvatarSession.start() no longer accepts livekit_api_key / livekit_api_secret. AvatarSession shipped in livekit-agents 1.6.7, so this is a break to a published surface, not an unreleased one.

 await avatar.start(
     session,
     room=ctx.room,
     livekit_url="wss://...",
-    livekit_api_key="...",
-    livekit_api_secret="...",
 )

Nothing signs a token client-side any more, so only livekit_url remains — the provider still needs a URL to dial. Passing the removed arguments raises TypeError at the call site.

Dropping them rather than ignoring them is deliberate. The gateway signs with the inference project's key, so a room owned by a different project than LIVEKIT_INFERENCE_* points at is no longer supported. That configuration used to work (we minted for the room project; the gateway just forwarded it). Silently ignoring the old arguments would turn it into an unexplained join failure minutes later; a TypeError says what happened, where it happened.

Testing

uv run pytest tests/test_inference_avatar.py --unit — 30 passed. ruff format / ruff check clean; mypy -p livekit.agents clean (205 files).

  • Replaced test_start_mints_worker_token_with_expected_grants with test_start_sends_mint_inputs_and_no_token: asserts livekit_token is absent from the payload and that all four mint inputs (room_name, avatar_identity, avatar_name, agent_identity) are present, with a custom participant name flowing through as avatar_name.
  • Added test_start_without_livekit_url_raises — the URL is still required; the room project's key/secret are not.
  • Dropped the removed kwargs from the seven start() call sites; removed the now-unused jwt import.

Note for the reviewer (out of scope here)

examples/avatar/inference_agent.py calls inference.AvatarSession("lemonslice", image_url=..., prompt=...), but those go inside extra_kwargs (LemonSliceOptions) — the example raises TypeError on the constructor. It's pre-existing (example and class landed together in #6492, so it was never run) and unrelated to this change, so I left it alone rather than widening the diff. Worth a separate one-line fix.

🤖 Generated with Claude Code

@rosetta-livekit-bot
rosetta-livekit-bot Bot requested a review from a team as a code owner July 28, 2026 18:01
@changeset-bot

changeset-bot Bot commented Jul 28, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b6e5c09

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-hedra 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-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

@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.

Open in Devin Review

@shawnfeldman shawnfeldman 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.

Reviewed at b6e5c094. No Critical or Important issues — approving. The port is faithful and the wire contract is right; everything below is a suggestion.

What I verified

Checked out the head SHA and ran it rather than trusting the description:

  • pnpm --filter @livekit/agents build — clean, including the tsc --declaration emit (so this typechecks).
  • vitest run src/inference/avatar.test.ts36/36 pass.
  • Full package suite — 111 files, 1504 passed, 5 skipped, matching your numbers exactly.
  • eslint on both changed files — 0 problems. (The repo-wide 141 problems are pre-existing; the two files you touched are clean.)

Wire contract, field-by-field against the merged gateway (agent-gateway origin/main:pkg/handler/avatar.go, #1095 merged 17:54Z): the payload is exactly avatarRequest. Every field the gateway requires non-empty is guarded locally first — livekit_url at avatar.ts:199, room_name at :209, agent_identity at :229, and avatar_identity by the constructor default. avatar_name lines up with the gateway's name == "" → AvatarIdentity fallback, so the two agree in both directions. livekit_token is gone on both sides.

Attribute parity survives the move, which is the part worth being sure about: mintAvatarWorkerToken still stamps lk.publish_on_behalf and lk.avatar_provider and keeps kind=agent. That matters because room_io.ts:251 reads lk.publish_on_behalf to skip the avatar when picking the participant to listen to — a silent drop there would have the agent transcribing its own avatar.

Port fidelity to agents#6575 is 1:1 on the payload dict. Keeping livekitApiKey/livekitApiSecret where Python deleted them is the right call — Python has no RoomServiceClient SID fallback, so it never needed them — and the moved credential check is a strict relaxation: no configuration that used to succeed now fails.

Deleting the JWT-claims test is also correct. That coverage moved server-side into #1095's own tests, and the JS side keeps assertions on every input the gateway mints from. not.toHaveProperty('livekit_token') is a real regression guard.

Suggestions

1. The class TSDoc dropped the cross-project caveat that the Python counterpart added. agents/src/inference/avatar.ts:96-102

agents@0534f5e89's docstring ends with:

Because the gateway signs the worker token with the key it authenticated, the project that serves inference must be the project that owns the room — pointing LIVEKIT_INFERENCE_* at a different project than the room is no longer supported.

That's a real consequence of this change: the SDK used to sign with the room's LIVEKIT_API_KEY, and the gateway now signs with whatever project it authenticated. A user with LIVEKIT_INFERENCE_* pointed at a different project than the room gets a worker token the room's server rejects, and the only symptom is waitForJoin() timing out. The JS class doc was rewritten in this PR but stops at "lets the gateway mint the avatar worker's room token," and the changeset doesn't mention it either. Since the endpoint is behind a default-off flag there's no installed base to break, so this is prospective guidance rather than a migration warning — but it's the one substantive thing the port left on the floor, and the "no source diff files were skipped" table reads as if it came across. Worth carrying that sentence (and ideally the lk.publish_on_behalf / lk.avatar_provider paragraph above it) into the TSDoc.

2. Nothing executes the code this PR moved. agents/src/inference/avatar.ts:527-533

The relocated credential check and the RoomServiceClient.listRooms fallback below it are both unreached by all 36 tests — the if (sid) return sid false branch is never taken. FakeConnectedRoom.getSid() returns 'RM_789', and the only fake returning '' is either disconnected (throws at :225 first) or has jobCtx.job.room.sid short-circuiting the || at :220. The listRooms half was already uncovered before this PR, but the credential throw is new, and it's the single place the JS diverges from the Python original.

Reachable cheaply via the job path: job: { room: { name: 'job-room' } } with no sid plus FakeJobRoom (inherited getSid() → ''), with LIVEKIT_API_KEY/LIVEKIT_API_SECRET deleted, asserting rejects.toThrow(/to resolve room sid/). A sibling test passing the keys and spying RoomServiceClient.prototype.listRooms closes the older gap too — the file already spies that prototype at avatar.test.ts:549.

3. avatar_identity normalization now differs between the SDK and the gateway. avatar.ts:156-158 vs the gateway's normalize()

The gateway does strings.TrimSpace(req.AvatarIdentity) and mints with the trimmed value; the SDK keeps its untrimmed copy for destinationIdentity (:267) and the join-wait. Before this PR both sides used the same untrimmed string because the SDK minted the token, so this is newly load-bearing. With whitespace in a configured avatarParticipantIdentity, the worker joins as bob while the SDK waits for ' bob ' — and waitForParticipant has no timeout, only an abort signal, so it hangs with no error and no log past a debug, while the provider session bills. Needs a pathological input, hence a suggestion, but it's zero-diagnosability when it does happen.

Cheap fix is .trim() in the constructor. The better one: the gateway already returns the authoritative avatar_identity in its 201 body, and CreateSessionResponse (:77-82) drops it — picking it up and warning on mismatch closes this whole class of drift, and would also cover suggestion 5.

4. livekit_url is asserted nowhere. avatar.test.ts:239-249

The string doesn't appear in the test file at all, only livekitUrl as an input. The gateway 400s without it, so a rename or drop of that payload key leaves the suite green and breaks every avatar create. Pre-existing, but you're rewriting exactly that toMatchObject block, so it's a one-line add.

5. Smaller things, all optional:

  • avatar.ts:184-186 — the sync-guard comment's parenthetical still lists "token mint" as one of the awaits before the ids are assigned. The load-bearing claim is still correct; the example list is now super.start() / sid resolution / gateway create.
  • avatar.ts:342-344 — the new payload comment enumerates roomName / avatar identity / agent identity, but omits provider → lk.avatar_provider, which is precisely the attribute whose local setter this PR deletes. A reader can't tell it survived.
  • avatar.ts:515-519resolveRoomSid takes the whole AvatarSessionStartOptions to read two members; Pick<AvatarSessionStartOptions, 'livekitApiKey' | 'livekitApiSecret'> narrows it without giving up the lazy env fallback.
  • avatar.test.ts:413-425'start without livekitUrl raises' passes no fetch override, so if that guard is ever reordered the test issues a real POST and fails as an opaque timeout instead of an assertion. expect(fetchMock).not.toHaveBeenCalled() pins it.
  • avatar.ts:232-234 — JS still hard-requires room_sid where the gateway marks it omitempty and Python passes an empty string through. I'd leave this alone: the gateway forwards RoomSID into LemonSlice's livekit_session_id unconditionally and nothing confirms the provider tolerates empty. Worth a note to the gateway owner rather than a change here.

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