@livekit/agents-plugin-lemonslice 1.0.50 → 1.0.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -28,6 +28,9 @@ import { AvatarSession } from '@livekit/agents-plugin-lemonslice';
28
28
  const avatar = new AvatarSession({
29
29
  agentImageUrl: 'publicly-accessible-image-url',
30
30
  apiKey: 'your-lemonslice-api-key', // or set LEMONSLICE_API_KEY env var
31
+ extraPayload: {
32
+ aspect_ratio: '9x16',
33
+ },
31
34
  });
32
35
 
33
36
  // Start the avatar session after creating your agent session
@@ -49,12 +52,15 @@ Set `LEMONSLICE_API_KEY` and `LEMONSLICE_IMAGE_URL` to get up and running.
49
52
  |--------|------|-------------|
50
53
  | `agentId` | `string` | The LemonSlice agent ID to use. Either `agentId` or `agentImageUrl` must be provided. |
51
54
  | `agentImageUrl` | `AvatarImage` | A publicly accessible url to your avatar image. Either `agentId` or `agentImageUrl` must be provided. |
55
+ | `extraPayload` | `Record<string, unknown>` | Additional LemonSlice session payload fields to forward to LemonSlice. |
52
56
  | `apiUrl` | `string` | The LemonSlice API URL. Defaults to `LEMONSLICE_API_URL` env var or the default LemonSlice API endpoint. |
53
57
  | `apiKey` | `string` | The LemonSlice API key. Defaults to `LEMONSLICE_API_KEY` env var. |
54
58
  | `avatarParticipantIdentity` | `string` | The identity of the avatar participant in the room. Defaults to `'lemonslice-avatar-agent'`. |
55
59
  | `avatarParticipantName` | `string` | The name of the avatar participant in the room. Defaults to `'lemonslice-avatar-agent'`. |
56
60
  | `connOptions` | `APIConnectOptions` | Connection options for API requests (retry count, timeout, etc.). |
57
61
 
62
+ Use `extraPayload` for LemonSlice API fields that are not yet modeled directly by the SDK.
63
+
58
64
  ## Environment Variables
59
65
 
60
66
  | Variable | Description |
package/dist/avatar.cjs CHANGED
@@ -42,6 +42,7 @@ class AvatarSession {
42
42
  agentImageUrl;
43
43
  agentPrompt;
44
44
  idleTimeout;
45
+ extraPayload;
45
46
  apiUrl;
46
47
  apiKey;
47
48
  avatarParticipantIdentity;
@@ -65,6 +66,7 @@ class AvatarSession {
65
66
  }
66
67
  this.agentPrompt = options.agentPrompt ?? null;
67
68
  this.idleTimeout = options.idleTimeout ?? null;
69
+ this.extraPayload = options.extraPayload ?? null;
68
70
  this.apiUrl = options.apiUrl || process.env.LEMONSLICE_API_URL || DEFAULT_API_URL;
69
71
  this.apiKey = options.apiKey || process.env.LEMONSLICE_API_KEY || "";
70
72
  if (!this.apiKey) {
@@ -88,6 +90,7 @@ class AvatarSession {
88
90
  * @param room - The LiveKit room where the avatar will join
89
91
  * @param options - Optional LiveKit credentials (falls back to environment variables)
90
92
  * @throws LemonSliceException if LiveKit credentials are not available or if the avatar session fails to start
93
+ * @returns The session ID of the LemonSlice session
91
94
  */
92
95
  async start(agentSession, room, options = {}) {
93
96
  var _a;
@@ -129,13 +132,14 @@ class AvatarSession {
129
132
  };
130
133
  const livekitToken = await at.toJwt();
131
134
  this.#logger.debug("starting avatar session");
132
- await this.startAgent(livekitUrl, livekitToken);
135
+ const sessionId = await this.startAgent(livekitUrl, livekitToken);
133
136
  agentSession.output.audio = new import_agents.voice.DataStreamAudioOutput({
134
137
  room,
135
138
  destinationIdentity: this.avatarParticipantIdentity,
136
139
  sampleRate: SAMPLE_RATE,
137
140
  waitRemoteTrack: import_rtc_node.TrackKind.KIND_VIDEO
138
141
  });
142
+ return sessionId;
139
143
  }
140
144
  async startAgent(livekitUrl, livekitToken) {
141
145
  for (let i = 0; i <= this.connOptions.maxRetry; i++) {
@@ -159,6 +163,9 @@ class AvatarSession {
159
163
  if (this.idleTimeout !== null) {
160
164
  payload.idle_timeout = this.idleTimeout;
161
165
  }
166
+ if (this.extraPayload) {
167
+ Object.assign(payload, this.extraPayload);
168
+ }
162
169
  const response = await fetch(this.apiUrl, {
163
170
  method: "POST",
164
171
  headers: {
@@ -175,7 +182,8 @@ class AvatarSession {
175
182
  options: { statusCode: response.status, body: { error: text } }
176
183
  });
177
184
  }
178
- return;
185
+ const data = await response.json();
186
+ return data.session_id;
179
187
  } catch (e) {
180
188
  if (e instanceof import_agents.APIStatusError && !e.retryable) {
181
189
  throw e;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/avatar.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport {\n type APIConnectOptions,\n APIConnectionError,\n APIStatusError,\n DEFAULT_API_CONNECT_OPTIONS,\n getJobContext,\n intervalForRetry,\n voice,\n} from '@livekit/agents';\nimport type { Room } from '@livekit/rtc-node';\nimport { TrackKind } from '@livekit/rtc-node';\nimport type { VideoGrant } from 'livekit-server-sdk';\nimport { AccessToken } from 'livekit-server-sdk';\nimport { log } from './log.js';\n\nconst ATTRIBUTE_PUBLISH_ON_BEHALF = 'lk.publish_on_behalf';\nconst DEFAULT_API_URL = 'https://lemonslice.com/api/liveai/sessions';\nconst SAMPLE_RATE = 16000;\nconst AVATAR_AGENT_IDENTITY = 'lemonslice-avatar-agent';\nconst AVATAR_AGENT_NAME = 'lemonslice-avatar-agent';\n\n/**\n * Exception thrown when there are errors with the LemonSlice API.\n */\nexport class LemonSliceException extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'LemonSliceException';\n }\n}\n\n/**\n * Options for configuring an AvatarSession.\n */\nexport interface AvatarSessionOptions {\n /**\n * The ID of the LemonSlice agent to add to the session.\n * Either agentId or agentImageUrl must be provided.\n */\n agentId?: string | null;\n /**\n * The URL of the image to use as the agent's avatar.\n * Either agentId or agentImageUrl must be provided.\n */\n agentImageUrl?: string | null;\n /**\n * A prompt that subtly influences the avatar's movements and expressions.\n */\n agentPrompt?: string | null;\n /**\n * The idle timeout, in seconds. Defaults to 60 seconds.\n */\n idleTimeout?: number | null;\n /**\n * The LemonSlice API URL. Defaults to https://lemonslice.com/api/liveai/sessions\n * or LEMONSLICE_API_URL environment variable.\n */\n apiUrl?: string;\n /**\n * The LemonSlice API key. Can also be set via LEMONSLICE_API_KEY environment variable.\n */\n apiKey?: string;\n /**\n * The identity of the avatar participant in the room. Defaults to 'lemonslice-avatar-agent'.\n */\n avatarParticipantIdentity?: string;\n /**\n * The name of the avatar participant in the room. Defaults to 'lemonslice-avatar-agent'.\n */\n avatarParticipantName?: string;\n /**\n * Connection options for API requests.\n */\n connOptions?: APIConnectOptions;\n}\n\n/**\n * Options for starting an avatar session.\n */\nexport interface StartOptions {\n /**\n * LiveKit server URL. Falls back to LIVEKIT_URL environment variable.\n */\n livekitUrl?: string;\n /**\n * LiveKit API key. Falls back to LIVEKIT_API_KEY environment variable.\n */\n livekitApiKey?: string;\n /**\n * LiveKit API secret. Falls back to LIVEKIT_API_SECRET environment variable.\n */\n livekitApiSecret?: string;\n}\n\n/**\n * A LemonSlice avatar session.\n *\n * This class manages the connection between a LiveKit agent and a LemonSlice avatar,\n * routing agent audio output to the avatar for visual representation.\n *\n * @example\n * ```typescript\n * // Using an agent ID\n * const avatar = new AvatarSession({\n * agentId: 'your-agent-id',\n * apiKey: 'your-lemonslice-api-key',\n * });\n * await avatar.start(agentSession, room);\n *\n * // Using a custom avatar image\n * const avatar = new AvatarSession({\n * agentImageUrl: 'your-image-url',\n * apiKey: 'your-lemonslice-api-key',\n * });\n * await avatar.start(agentSession, room);\n * ```\n */\nexport class AvatarSession {\n private agentId: string | null;\n private agentImageUrl: string | null;\n private agentPrompt: string | null;\n private idleTimeout: number | null;\n private apiUrl: string;\n private apiKey: string;\n private avatarParticipantIdentity: string;\n private avatarParticipantName: string;\n private connOptions: APIConnectOptions;\n\n #logger = log();\n\n /**\n * Creates a new AvatarSession.\n *\n * @param options - Configuration options for the avatar session\n * @throws LemonSliceException if invalid agentId or agentImageUrl is provided, or if LemonSlice API key is not set\n */\n constructor(options: AvatarSessionOptions = {}) {\n this.agentId = options.agentId ?? null;\n this.agentImageUrl = options.agentImageUrl ?? null;\n\n if (!this.agentId && !this.agentImageUrl) {\n throw new LemonSliceException('Missing agentId or agentImageUrl');\n }\n if (this.agentId && this.agentImageUrl) {\n throw new LemonSliceException('Only one of agentId or agentImageUrl can be provided');\n }\n\n this.agentPrompt = options.agentPrompt ?? null;\n this.idleTimeout = options.idleTimeout ?? null;\n\n this.apiUrl = options.apiUrl || process.env.LEMONSLICE_API_URL || DEFAULT_API_URL;\n this.apiKey = options.apiKey || process.env.LEMONSLICE_API_KEY || '';\n\n if (!this.apiKey) {\n throw new LemonSliceException(\n 'The api_key must be set either by passing apiKey to the client or ' +\n 'by setting the LEMONSLICE_API_KEY environment variable',\n );\n }\n\n this.avatarParticipantIdentity = options.avatarParticipantIdentity || AVATAR_AGENT_IDENTITY;\n this.avatarParticipantName = options.avatarParticipantName || AVATAR_AGENT_NAME;\n this.connOptions = options.connOptions || DEFAULT_API_CONNECT_OPTIONS;\n }\n\n /**\n * Starts the avatar session and connects it to the agent.\n *\n * This method:\n * 1. Creates a LiveKit token for the avatar participant\n * 2. Calls the LemonSlice API to start the avatar session\n * 3. Configures the agent's audio output to stream to the avatar\n *\n * @param agentSession - The agent session to connect to the avatar\n * @param room - The LiveKit room where the avatar will join\n * @param options - Optional LiveKit credentials (falls back to environment variables)\n * @throws LemonSliceException if LiveKit credentials are not available or if the avatar session fails to start\n */\n async start(\n agentSession: voice.AgentSession,\n room: Room,\n options: StartOptions = {},\n ): Promise<void> {\n const livekitUrl = options.livekitUrl || process.env.LIVEKIT_URL;\n const livekitApiKey = options.livekitApiKey || process.env.LIVEKIT_API_KEY;\n const livekitApiSecret = options.livekitApiSecret || process.env.LIVEKIT_API_SECRET;\n\n if (!livekitUrl || !livekitApiKey || !livekitApiSecret) {\n throw new LemonSliceException(\n 'livekitUrl, livekitApiKey, and livekitApiSecret must be set ' +\n 'by arguments or environment variables',\n );\n }\n\n let localParticipantIdentity: string;\n try {\n const jobCtx = getJobContext();\n localParticipantIdentity = jobCtx.agent?.identity || '';\n if (!localParticipantIdentity && room.localParticipant) {\n localParticipantIdentity = room.localParticipant.identity;\n }\n } catch {\n if (!room.isConnected || !room.localParticipant) {\n throw new LemonSliceException('failed to get local participant identity');\n }\n localParticipantIdentity = room.localParticipant.identity;\n }\n\n if (!localParticipantIdentity) {\n throw new LemonSliceException('failed to get local participant identity');\n }\n\n const at = new AccessToken(livekitApiKey, livekitApiSecret, {\n identity: this.avatarParticipantIdentity,\n name: this.avatarParticipantName,\n });\n at.kind = 'agent';\n\n at.addGrant({\n roomJoin: true,\n room: room.name,\n } as VideoGrant);\n\n // allow the avatar agent to publish audio and video on behalf of your local agent\n at.attributes = {\n [ATTRIBUTE_PUBLISH_ON_BEHALF]: localParticipantIdentity,\n };\n\n const livekitToken = await at.toJwt();\n\n this.#logger.debug('starting avatar session');\n await this.startAgent(livekitUrl, livekitToken);\n\n agentSession.output.audio = new voice.DataStreamAudioOutput({\n room,\n destinationIdentity: this.avatarParticipantIdentity,\n sampleRate: SAMPLE_RATE,\n waitRemoteTrack: TrackKind.KIND_VIDEO,\n });\n }\n\n private async startAgent(livekitUrl: string, livekitToken: string): Promise<void> {\n for (let i = 0; i <= this.connOptions.maxRetry; i++) {\n try {\n const payload: Record<string, any> = {\n transport_type: 'livekit',\n properties: {\n livekit_url: livekitUrl,\n livekit_token: livekitToken,\n },\n };\n\n if (this.agentId) {\n payload.agent_id = this.agentId;\n }\n\n if (this.agentImageUrl) {\n payload.agent_image_url = this.agentImageUrl;\n }\n\n if (this.agentPrompt) {\n payload.agent_prompt = this.agentPrompt;\n }\n\n if (this.idleTimeout !== null) {\n payload.idle_timeout = this.idleTimeout;\n }\n\n const response = await fetch(this.apiUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': this.apiKey,\n },\n body: JSON.stringify(payload),\n signal: AbortSignal.timeout(this.connOptions.timeoutMs),\n });\n\n if (!response.ok) {\n const text = await response.text();\n throw new APIStatusError({\n message: 'Server returned an error',\n options: { statusCode: response.status, body: { error: text } },\n });\n }\n return;\n } catch (e) {\n if (e instanceof APIStatusError && !e.retryable) {\n throw e;\n }\n if (e instanceof APIConnectionError) {\n this.#logger.warn({ error: String(e) }, 'failed to call lemonslice api');\n } else {\n this.#logger.error({ error: e }, 'failed to call lemonslice api');\n }\n\n if (i <= this.connOptions.maxRetry - 1) {\n await new Promise((resolve) =>\n setTimeout(resolve, intervalForRetry(this.connOptions, i)),\n );\n }\n }\n }\n\n throw new APIConnectionError({\n message: 'Failed to start LemonSlice Avatar Session after all retries',\n });\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/avatar.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport {\n type APIConnectOptions,\n APIConnectionError,\n APIStatusError,\n DEFAULT_API_CONNECT_OPTIONS,\n getJobContext,\n intervalForRetry,\n voice,\n} from '@livekit/agents';\nimport type { Room } from '@livekit/rtc-node';\nimport { TrackKind } from '@livekit/rtc-node';\nimport type { VideoGrant } from 'livekit-server-sdk';\nimport { AccessToken } from 'livekit-server-sdk';\nimport { log } from './log.js';\n\nconst ATTRIBUTE_PUBLISH_ON_BEHALF = 'lk.publish_on_behalf';\nconst DEFAULT_API_URL = 'https://lemonslice.com/api/liveai/sessions';\nconst SAMPLE_RATE = 16000;\nconst AVATAR_AGENT_IDENTITY = 'lemonslice-avatar-agent';\nconst AVATAR_AGENT_NAME = 'lemonslice-avatar-agent';\n\n/**\n * Exception thrown when there are errors with the LemonSlice API.\n */\nexport class LemonSliceException extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'LemonSliceException';\n }\n}\n\n/**\n * Options for configuring an AvatarSession.\n */\nexport interface AvatarSessionOptions {\n /**\n * The ID of the LemonSlice agent to add to the session.\n * Either agentId or agentImageUrl must be provided.\n */\n agentId?: string | null;\n /**\n * The URL of the image to use as the agent's avatar.\n * Either agentId or agentImageUrl must be provided.\n */\n agentImageUrl?: string | null;\n /**\n * A prompt that subtly influences the avatar's movements and expressions.\n */\n agentPrompt?: string | null;\n /**\n * The idle timeout, in seconds. Defaults to 60 seconds.\n */\n idleTimeout?: number | null;\n /**\n * Additional payload fields to merge into the LemonSlice session creation request.\n */\n extraPayload?: Record<string, unknown> | null;\n /**\n * The LemonSlice API URL. Defaults to https://lemonslice.com/api/liveai/sessions\n * or LEMONSLICE_API_URL environment variable.\n */\n apiUrl?: string;\n /**\n * The LemonSlice API key. Can also be set via LEMONSLICE_API_KEY environment variable.\n */\n apiKey?: string;\n /**\n * The identity of the avatar participant in the room. Defaults to 'lemonslice-avatar-agent'.\n */\n avatarParticipantIdentity?: string;\n /**\n * The name of the avatar participant in the room. Defaults to 'lemonslice-avatar-agent'.\n */\n avatarParticipantName?: string;\n /**\n * Connection options for API requests.\n */\n connOptions?: APIConnectOptions;\n}\n\n/**\n * Options for starting an avatar session.\n */\nexport interface StartOptions {\n /**\n * LiveKit server URL. Falls back to LIVEKIT_URL environment variable.\n */\n livekitUrl?: string;\n /**\n * LiveKit API key. Falls back to LIVEKIT_API_KEY environment variable.\n */\n livekitApiKey?: string;\n /**\n * LiveKit API secret. Falls back to LIVEKIT_API_SECRET environment variable.\n */\n livekitApiSecret?: string;\n}\n\n/**\n * A LemonSlice avatar session.\n *\n * This class manages the connection between a LiveKit agent and a LemonSlice avatar,\n * routing agent audio output to the avatar for visual representation.\n *\n * @example\n * ```typescript\n * // Using an agent ID\n * const avatar = new AvatarSession({\n * agentId: 'your-agent-id',\n * apiKey: 'your-lemonslice-api-key',\n * });\n * await avatar.start(agentSession, room);\n *\n * // Using a custom avatar image\n * const avatar = new AvatarSession({\n * agentImageUrl: 'your-image-url',\n * apiKey: 'your-lemonslice-api-key',\n * });\n * await avatar.start(agentSession, room);\n * ```\n */\nexport class AvatarSession {\n private agentId: string | null;\n private agentImageUrl: string | null;\n private agentPrompt: string | null;\n private idleTimeout: number | null;\n private extraPayload: Record<string, unknown> | null;\n private apiUrl: string;\n private apiKey: string;\n private avatarParticipantIdentity: string;\n private avatarParticipantName: string;\n private connOptions: APIConnectOptions;\n\n #logger = log();\n\n /**\n * Creates a new AvatarSession.\n *\n * @param options - Configuration options for the avatar session\n * @throws LemonSliceException if invalid agentId or agentImageUrl is provided, or if LemonSlice API key is not set\n */\n constructor(options: AvatarSessionOptions = {}) {\n this.agentId = options.agentId ?? null;\n this.agentImageUrl = options.agentImageUrl ?? null;\n\n if (!this.agentId && !this.agentImageUrl) {\n throw new LemonSliceException('Missing agentId or agentImageUrl');\n }\n if (this.agentId && this.agentImageUrl) {\n throw new LemonSliceException('Only one of agentId or agentImageUrl can be provided');\n }\n\n this.agentPrompt = options.agentPrompt ?? null;\n this.idleTimeout = options.idleTimeout ?? null;\n this.extraPayload = options.extraPayload ?? null;\n\n this.apiUrl = options.apiUrl || process.env.LEMONSLICE_API_URL || DEFAULT_API_URL;\n this.apiKey = options.apiKey || process.env.LEMONSLICE_API_KEY || '';\n\n if (!this.apiKey) {\n throw new LemonSliceException(\n 'The api_key must be set either by passing apiKey to the client or ' +\n 'by setting the LEMONSLICE_API_KEY environment variable',\n );\n }\n\n this.avatarParticipantIdentity = options.avatarParticipantIdentity || AVATAR_AGENT_IDENTITY;\n this.avatarParticipantName = options.avatarParticipantName || AVATAR_AGENT_NAME;\n this.connOptions = options.connOptions || DEFAULT_API_CONNECT_OPTIONS;\n }\n\n /**\n * Starts the avatar session and connects it to the agent.\n *\n * This method:\n * 1. Creates a LiveKit token for the avatar participant\n * 2. Calls the LemonSlice API to start the avatar session\n * 3. Configures the agent's audio output to stream to the avatar\n *\n * @param agentSession - The agent session to connect to the avatar\n * @param room - The LiveKit room where the avatar will join\n * @param options - Optional LiveKit credentials (falls back to environment variables)\n * @throws LemonSliceException if LiveKit credentials are not available or if the avatar session fails to start\n * @returns The session ID of the LemonSlice session\n */\n async start(\n agentSession: voice.AgentSession,\n room: Room,\n options: StartOptions = {},\n ): Promise<string> {\n const livekitUrl = options.livekitUrl || process.env.LIVEKIT_URL;\n const livekitApiKey = options.livekitApiKey || process.env.LIVEKIT_API_KEY;\n const livekitApiSecret = options.livekitApiSecret || process.env.LIVEKIT_API_SECRET;\n\n if (!livekitUrl || !livekitApiKey || !livekitApiSecret) {\n throw new LemonSliceException(\n 'livekitUrl, livekitApiKey, and livekitApiSecret must be set ' +\n 'by arguments or environment variables',\n );\n }\n\n let localParticipantIdentity: string;\n try {\n const jobCtx = getJobContext();\n localParticipantIdentity = jobCtx.agent?.identity || '';\n if (!localParticipantIdentity && room.localParticipant) {\n localParticipantIdentity = room.localParticipant.identity;\n }\n } catch {\n if (!room.isConnected || !room.localParticipant) {\n throw new LemonSliceException('failed to get local participant identity');\n }\n localParticipantIdentity = room.localParticipant.identity;\n }\n\n if (!localParticipantIdentity) {\n throw new LemonSliceException('failed to get local participant identity');\n }\n\n const at = new AccessToken(livekitApiKey, livekitApiSecret, {\n identity: this.avatarParticipantIdentity,\n name: this.avatarParticipantName,\n });\n at.kind = 'agent';\n\n at.addGrant({\n roomJoin: true,\n room: room.name,\n } as VideoGrant);\n\n // allow the avatar agent to publish audio and video on behalf of your local agent\n at.attributes = {\n [ATTRIBUTE_PUBLISH_ON_BEHALF]: localParticipantIdentity,\n };\n\n const livekitToken = await at.toJwt();\n\n this.#logger.debug('starting avatar session');\n const sessionId = await this.startAgent(livekitUrl, livekitToken);\n\n agentSession.output.audio = new voice.DataStreamAudioOutput({\n room,\n destinationIdentity: this.avatarParticipantIdentity,\n sampleRate: SAMPLE_RATE,\n waitRemoteTrack: TrackKind.KIND_VIDEO,\n });\n\n return sessionId;\n }\n\n private async startAgent(livekitUrl: string, livekitToken: string): Promise<string> {\n for (let i = 0; i <= this.connOptions.maxRetry; i++) {\n try {\n const payload: Record<string, any> = {\n transport_type: 'livekit',\n properties: {\n livekit_url: livekitUrl,\n livekit_token: livekitToken,\n },\n };\n\n if (this.agentId) {\n payload.agent_id = this.agentId;\n }\n\n if (this.agentImageUrl) {\n payload.agent_image_url = this.agentImageUrl;\n }\n\n if (this.agentPrompt) {\n payload.agent_prompt = this.agentPrompt;\n }\n\n if (this.idleTimeout !== null) {\n payload.idle_timeout = this.idleTimeout;\n }\n\n if (this.extraPayload) {\n Object.assign(payload, this.extraPayload);\n }\n\n const response = await fetch(this.apiUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': this.apiKey,\n },\n body: JSON.stringify(payload),\n signal: AbortSignal.timeout(this.connOptions.timeoutMs),\n });\n\n if (!response.ok) {\n const text = await response.text();\n throw new APIStatusError({\n message: 'Server returned an error',\n options: { statusCode: response.status, body: { error: text } },\n });\n }\n const data = (await response.json()) as { session_id: string };\n return data.session_id;\n } catch (e) {\n if (e instanceof APIStatusError && !e.retryable) {\n throw e;\n }\n if (e instanceof APIConnectionError) {\n this.#logger.warn({ error: String(e) }, 'failed to call lemonslice api');\n } else {\n this.#logger.error({ error: e }, 'failed to call lemonslice api');\n }\n\n if (i <= this.connOptions.maxRetry - 1) {\n await new Promise((resolve) =>\n setTimeout(resolve, intervalForRetry(this.connOptions, i)),\n );\n }\n }\n }\n\n throw new APIConnectionError({\n message: 'Failed to start LemonSlice Avatar Session after all retries',\n });\n }\n}\n"],"mappings":"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","names":[]}
package/dist/avatar.d.cts CHANGED
@@ -28,6 +28,10 @@ export interface AvatarSessionOptions {
28
28
  * The idle timeout, in seconds. Defaults to 60 seconds.
29
29
  */
30
30
  idleTimeout?: number | null;
31
+ /**
32
+ * Additional payload fields to merge into the LemonSlice session creation request.
33
+ */
34
+ extraPayload?: Record<string, unknown> | null;
31
35
  /**
32
36
  * The LemonSlice API URL. Defaults to https://lemonslice.com/api/liveai/sessions
33
37
  * or LEMONSLICE_API_URL environment variable.
@@ -96,6 +100,7 @@ export declare class AvatarSession {
96
100
  private agentImageUrl;
97
101
  private agentPrompt;
98
102
  private idleTimeout;
103
+ private extraPayload;
99
104
  private apiUrl;
100
105
  private apiKey;
101
106
  private avatarParticipantIdentity;
@@ -120,8 +125,9 @@ export declare class AvatarSession {
120
125
  * @param room - The LiveKit room where the avatar will join
121
126
  * @param options - Optional LiveKit credentials (falls back to environment variables)
122
127
  * @throws LemonSliceException if LiveKit credentials are not available or if the avatar session fails to start
128
+ * @returns The session ID of the LemonSlice session
123
129
  */
124
- start(agentSession: voice.AgentSession, room: Room, options?: StartOptions): Promise<void>;
130
+ start(agentSession: voice.AgentSession, room: Room, options?: StartOptions): Promise<string>;
125
131
  private startAgent;
126
132
  }
127
133
  //# sourceMappingURL=avatar.d.ts.map
package/dist/avatar.d.ts CHANGED
@@ -28,6 +28,10 @@ export interface AvatarSessionOptions {
28
28
  * The idle timeout, in seconds. Defaults to 60 seconds.
29
29
  */
30
30
  idleTimeout?: number | null;
31
+ /**
32
+ * Additional payload fields to merge into the LemonSlice session creation request.
33
+ */
34
+ extraPayload?: Record<string, unknown> | null;
31
35
  /**
32
36
  * The LemonSlice API URL. Defaults to https://lemonslice.com/api/liveai/sessions
33
37
  * or LEMONSLICE_API_URL environment variable.
@@ -96,6 +100,7 @@ export declare class AvatarSession {
96
100
  private agentImageUrl;
97
101
  private agentPrompt;
98
102
  private idleTimeout;
103
+ private extraPayload;
99
104
  private apiUrl;
100
105
  private apiKey;
101
106
  private avatarParticipantIdentity;
@@ -120,8 +125,9 @@ export declare class AvatarSession {
120
125
  * @param room - The LiveKit room where the avatar will join
121
126
  * @param options - Optional LiveKit credentials (falls back to environment variables)
122
127
  * @throws LemonSliceException if LiveKit credentials are not available or if the avatar session fails to start
128
+ * @returns The session ID of the LemonSlice session
123
129
  */
124
- start(agentSession: voice.AgentSession, room: Room, options?: StartOptions): Promise<void>;
130
+ start(agentSession: voice.AgentSession, room: Room, options?: StartOptions): Promise<string>;
125
131
  private startAgent;
126
132
  }
127
133
  //# sourceMappingURL=avatar.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"avatar.d.ts","sourceRoot":"","sources":["../src/avatar.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,iBAAiB,EAMtB,KAAK,EACN,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAY9C;;GAEG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;OAEG;IACH,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC;;OAEG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;OAEG;IACH,WAAW,CAAC,EAAE,iBAAiB,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,aAAa;;IACxB,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,WAAW,CAAgB;IACnC,OAAO,CAAC,WAAW,CAAgB;IACnC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,yBAAyB,CAAS;IAC1C,OAAO,CAAC,qBAAqB,CAAS;IACtC,OAAO,CAAC,WAAW,CAAoB;IAIvC;;;;;OAKG;gBACS,OAAO,GAAE,oBAAyB;IA6B9C;;;;;;;;;;;;OAYG;IACG,KAAK,CACT,YAAY,EAAE,KAAK,CAAC,YAAY,EAChC,IAAI,EAAE,IAAI,EACV,OAAO,GAAE,YAAiB,GACzB,OAAO,CAAC,IAAI,CAAC;YA2DF,UAAU;CAmEzB"}
1
+ {"version":3,"file":"avatar.d.ts","sourceRoot":"","sources":["../src/avatar.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,iBAAiB,EAMtB,KAAK,EACN,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAY9C;;GAEG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC9C;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;OAEG;IACH,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC;;OAEG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;OAEG;IACH,WAAW,CAAC,EAAE,iBAAiB,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,aAAa;;IACxB,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,WAAW,CAAgB;IACnC,OAAO,CAAC,WAAW,CAAgB;IACnC,OAAO,CAAC,YAAY,CAAiC;IACrD,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,yBAAyB,CAAS;IAC1C,OAAO,CAAC,qBAAqB,CAAS;IACtC,OAAO,CAAC,WAAW,CAAoB;IAIvC;;;;;OAKG;gBACS,OAAO,GAAE,oBAAyB;IA8B9C;;;;;;;;;;;;;OAaG;IACG,KAAK,CACT,YAAY,EAAE,KAAK,CAAC,YAAY,EAChC,IAAI,EAAE,IAAI,EACV,OAAO,GAAE,YAAiB,GACzB,OAAO,CAAC,MAAM,CAAC;YA6DJ,UAAU;CAwEzB"}
package/dist/avatar.js CHANGED
@@ -25,6 +25,7 @@ class AvatarSession {
25
25
  agentImageUrl;
26
26
  agentPrompt;
27
27
  idleTimeout;
28
+ extraPayload;
28
29
  apiUrl;
29
30
  apiKey;
30
31
  avatarParticipantIdentity;
@@ -48,6 +49,7 @@ class AvatarSession {
48
49
  }
49
50
  this.agentPrompt = options.agentPrompt ?? null;
50
51
  this.idleTimeout = options.idleTimeout ?? null;
52
+ this.extraPayload = options.extraPayload ?? null;
51
53
  this.apiUrl = options.apiUrl || process.env.LEMONSLICE_API_URL || DEFAULT_API_URL;
52
54
  this.apiKey = options.apiKey || process.env.LEMONSLICE_API_KEY || "";
53
55
  if (!this.apiKey) {
@@ -71,6 +73,7 @@ class AvatarSession {
71
73
  * @param room - The LiveKit room where the avatar will join
72
74
  * @param options - Optional LiveKit credentials (falls back to environment variables)
73
75
  * @throws LemonSliceException if LiveKit credentials are not available or if the avatar session fails to start
76
+ * @returns The session ID of the LemonSlice session
74
77
  */
75
78
  async start(agentSession, room, options = {}) {
76
79
  var _a;
@@ -112,13 +115,14 @@ class AvatarSession {
112
115
  };
113
116
  const livekitToken = await at.toJwt();
114
117
  this.#logger.debug("starting avatar session");
115
- await this.startAgent(livekitUrl, livekitToken);
118
+ const sessionId = await this.startAgent(livekitUrl, livekitToken);
116
119
  agentSession.output.audio = new voice.DataStreamAudioOutput({
117
120
  room,
118
121
  destinationIdentity: this.avatarParticipantIdentity,
119
122
  sampleRate: SAMPLE_RATE,
120
123
  waitRemoteTrack: TrackKind.KIND_VIDEO
121
124
  });
125
+ return sessionId;
122
126
  }
123
127
  async startAgent(livekitUrl, livekitToken) {
124
128
  for (let i = 0; i <= this.connOptions.maxRetry; i++) {
@@ -142,6 +146,9 @@ class AvatarSession {
142
146
  if (this.idleTimeout !== null) {
143
147
  payload.idle_timeout = this.idleTimeout;
144
148
  }
149
+ if (this.extraPayload) {
150
+ Object.assign(payload, this.extraPayload);
151
+ }
145
152
  const response = await fetch(this.apiUrl, {
146
153
  method: "POST",
147
154
  headers: {
@@ -158,7 +165,8 @@ class AvatarSession {
158
165
  options: { statusCode: response.status, body: { error: text } }
159
166
  });
160
167
  }
161
- return;
168
+ const data = await response.json();
169
+ return data.session_id;
162
170
  } catch (e) {
163
171
  if (e instanceof APIStatusError && !e.retryable) {
164
172
  throw e;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/avatar.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport {\n type APIConnectOptions,\n APIConnectionError,\n APIStatusError,\n DEFAULT_API_CONNECT_OPTIONS,\n getJobContext,\n intervalForRetry,\n voice,\n} from '@livekit/agents';\nimport type { Room } from '@livekit/rtc-node';\nimport { TrackKind } from '@livekit/rtc-node';\nimport type { VideoGrant } from 'livekit-server-sdk';\nimport { AccessToken } from 'livekit-server-sdk';\nimport { log } from './log.js';\n\nconst ATTRIBUTE_PUBLISH_ON_BEHALF = 'lk.publish_on_behalf';\nconst DEFAULT_API_URL = 'https://lemonslice.com/api/liveai/sessions';\nconst SAMPLE_RATE = 16000;\nconst AVATAR_AGENT_IDENTITY = 'lemonslice-avatar-agent';\nconst AVATAR_AGENT_NAME = 'lemonslice-avatar-agent';\n\n/**\n * Exception thrown when there are errors with the LemonSlice API.\n */\nexport class LemonSliceException extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'LemonSliceException';\n }\n}\n\n/**\n * Options for configuring an AvatarSession.\n */\nexport interface AvatarSessionOptions {\n /**\n * The ID of the LemonSlice agent to add to the session.\n * Either agentId or agentImageUrl must be provided.\n */\n agentId?: string | null;\n /**\n * The URL of the image to use as the agent's avatar.\n * Either agentId or agentImageUrl must be provided.\n */\n agentImageUrl?: string | null;\n /**\n * A prompt that subtly influences the avatar's movements and expressions.\n */\n agentPrompt?: string | null;\n /**\n * The idle timeout, in seconds. Defaults to 60 seconds.\n */\n idleTimeout?: number | null;\n /**\n * The LemonSlice API URL. Defaults to https://lemonslice.com/api/liveai/sessions\n * or LEMONSLICE_API_URL environment variable.\n */\n apiUrl?: string;\n /**\n * The LemonSlice API key. Can also be set via LEMONSLICE_API_KEY environment variable.\n */\n apiKey?: string;\n /**\n * The identity of the avatar participant in the room. Defaults to 'lemonslice-avatar-agent'.\n */\n avatarParticipantIdentity?: string;\n /**\n * The name of the avatar participant in the room. Defaults to 'lemonslice-avatar-agent'.\n */\n avatarParticipantName?: string;\n /**\n * Connection options for API requests.\n */\n connOptions?: APIConnectOptions;\n}\n\n/**\n * Options for starting an avatar session.\n */\nexport interface StartOptions {\n /**\n * LiveKit server URL. Falls back to LIVEKIT_URL environment variable.\n */\n livekitUrl?: string;\n /**\n * LiveKit API key. Falls back to LIVEKIT_API_KEY environment variable.\n */\n livekitApiKey?: string;\n /**\n * LiveKit API secret. Falls back to LIVEKIT_API_SECRET environment variable.\n */\n livekitApiSecret?: string;\n}\n\n/**\n * A LemonSlice avatar session.\n *\n * This class manages the connection between a LiveKit agent and a LemonSlice avatar,\n * routing agent audio output to the avatar for visual representation.\n *\n * @example\n * ```typescript\n * // Using an agent ID\n * const avatar = new AvatarSession({\n * agentId: 'your-agent-id',\n * apiKey: 'your-lemonslice-api-key',\n * });\n * await avatar.start(agentSession, room);\n *\n * // Using a custom avatar image\n * const avatar = new AvatarSession({\n * agentImageUrl: 'your-image-url',\n * apiKey: 'your-lemonslice-api-key',\n * });\n * await avatar.start(agentSession, room);\n * ```\n */\nexport class AvatarSession {\n private agentId: string | null;\n private agentImageUrl: string | null;\n private agentPrompt: string | null;\n private idleTimeout: number | null;\n private apiUrl: string;\n private apiKey: string;\n private avatarParticipantIdentity: string;\n private avatarParticipantName: string;\n private connOptions: APIConnectOptions;\n\n #logger = log();\n\n /**\n * Creates a new AvatarSession.\n *\n * @param options - Configuration options for the avatar session\n * @throws LemonSliceException if invalid agentId or agentImageUrl is provided, or if LemonSlice API key is not set\n */\n constructor(options: AvatarSessionOptions = {}) {\n this.agentId = options.agentId ?? null;\n this.agentImageUrl = options.agentImageUrl ?? null;\n\n if (!this.agentId && !this.agentImageUrl) {\n throw new LemonSliceException('Missing agentId or agentImageUrl');\n }\n if (this.agentId && this.agentImageUrl) {\n throw new LemonSliceException('Only one of agentId or agentImageUrl can be provided');\n }\n\n this.agentPrompt = options.agentPrompt ?? null;\n this.idleTimeout = options.idleTimeout ?? null;\n\n this.apiUrl = options.apiUrl || process.env.LEMONSLICE_API_URL || DEFAULT_API_URL;\n this.apiKey = options.apiKey || process.env.LEMONSLICE_API_KEY || '';\n\n if (!this.apiKey) {\n throw new LemonSliceException(\n 'The api_key must be set either by passing apiKey to the client or ' +\n 'by setting the LEMONSLICE_API_KEY environment variable',\n );\n }\n\n this.avatarParticipantIdentity = options.avatarParticipantIdentity || AVATAR_AGENT_IDENTITY;\n this.avatarParticipantName = options.avatarParticipantName || AVATAR_AGENT_NAME;\n this.connOptions = options.connOptions || DEFAULT_API_CONNECT_OPTIONS;\n }\n\n /**\n * Starts the avatar session and connects it to the agent.\n *\n * This method:\n * 1. Creates a LiveKit token for the avatar participant\n * 2. Calls the LemonSlice API to start the avatar session\n * 3. Configures the agent's audio output to stream to the avatar\n *\n * @param agentSession - The agent session to connect to the avatar\n * @param room - The LiveKit room where the avatar will join\n * @param options - Optional LiveKit credentials (falls back to environment variables)\n * @throws LemonSliceException if LiveKit credentials are not available or if the avatar session fails to start\n */\n async start(\n agentSession: voice.AgentSession,\n room: Room,\n options: StartOptions = {},\n ): Promise<void> {\n const livekitUrl = options.livekitUrl || process.env.LIVEKIT_URL;\n const livekitApiKey = options.livekitApiKey || process.env.LIVEKIT_API_KEY;\n const livekitApiSecret = options.livekitApiSecret || process.env.LIVEKIT_API_SECRET;\n\n if (!livekitUrl || !livekitApiKey || !livekitApiSecret) {\n throw new LemonSliceException(\n 'livekitUrl, livekitApiKey, and livekitApiSecret must be set ' +\n 'by arguments or environment variables',\n );\n }\n\n let localParticipantIdentity: string;\n try {\n const jobCtx = getJobContext();\n localParticipantIdentity = jobCtx.agent?.identity || '';\n if (!localParticipantIdentity && room.localParticipant) {\n localParticipantIdentity = room.localParticipant.identity;\n }\n } catch {\n if (!room.isConnected || !room.localParticipant) {\n throw new LemonSliceException('failed to get local participant identity');\n }\n localParticipantIdentity = room.localParticipant.identity;\n }\n\n if (!localParticipantIdentity) {\n throw new LemonSliceException('failed to get local participant identity');\n }\n\n const at = new AccessToken(livekitApiKey, livekitApiSecret, {\n identity: this.avatarParticipantIdentity,\n name: this.avatarParticipantName,\n });\n at.kind = 'agent';\n\n at.addGrant({\n roomJoin: true,\n room: room.name,\n } as VideoGrant);\n\n // allow the avatar agent to publish audio and video on behalf of your local agent\n at.attributes = {\n [ATTRIBUTE_PUBLISH_ON_BEHALF]: localParticipantIdentity,\n };\n\n const livekitToken = await at.toJwt();\n\n this.#logger.debug('starting avatar session');\n await this.startAgent(livekitUrl, livekitToken);\n\n agentSession.output.audio = new voice.DataStreamAudioOutput({\n room,\n destinationIdentity: this.avatarParticipantIdentity,\n sampleRate: SAMPLE_RATE,\n waitRemoteTrack: TrackKind.KIND_VIDEO,\n });\n }\n\n private async startAgent(livekitUrl: string, livekitToken: string): Promise<void> {\n for (let i = 0; i <= this.connOptions.maxRetry; i++) {\n try {\n const payload: Record<string, any> = {\n transport_type: 'livekit',\n properties: {\n livekit_url: livekitUrl,\n livekit_token: livekitToken,\n },\n };\n\n if (this.agentId) {\n payload.agent_id = this.agentId;\n }\n\n if (this.agentImageUrl) {\n payload.agent_image_url = this.agentImageUrl;\n }\n\n if (this.agentPrompt) {\n payload.agent_prompt = this.agentPrompt;\n }\n\n if (this.idleTimeout !== null) {\n payload.idle_timeout = this.idleTimeout;\n }\n\n const response = await fetch(this.apiUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': this.apiKey,\n },\n body: JSON.stringify(payload),\n signal: AbortSignal.timeout(this.connOptions.timeoutMs),\n });\n\n if (!response.ok) {\n const text = await response.text();\n throw new APIStatusError({\n message: 'Server returned an error',\n options: { statusCode: response.status, body: { error: text } },\n });\n }\n return;\n } catch (e) {\n if (e instanceof APIStatusError && !e.retryable) {\n throw e;\n }\n if (e instanceof APIConnectionError) {\n this.#logger.warn({ error: String(e) }, 'failed to call lemonslice api');\n } else {\n this.#logger.error({ error: e }, 'failed to call lemonslice api');\n }\n\n if (i <= this.connOptions.maxRetry - 1) {\n await new Promise((resolve) =>\n setTimeout(resolve, intervalForRetry(this.connOptions, i)),\n );\n }\n }\n }\n\n throw new APIConnectionError({\n message: 'Failed to start LemonSlice Avatar Session after all retries',\n });\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/avatar.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport {\n type APIConnectOptions,\n APIConnectionError,\n APIStatusError,\n DEFAULT_API_CONNECT_OPTIONS,\n getJobContext,\n intervalForRetry,\n voice,\n} from '@livekit/agents';\nimport type { Room } from '@livekit/rtc-node';\nimport { TrackKind } from '@livekit/rtc-node';\nimport type { VideoGrant } from 'livekit-server-sdk';\nimport { AccessToken } from 'livekit-server-sdk';\nimport { log } from './log.js';\n\nconst ATTRIBUTE_PUBLISH_ON_BEHALF = 'lk.publish_on_behalf';\nconst DEFAULT_API_URL = 'https://lemonslice.com/api/liveai/sessions';\nconst SAMPLE_RATE = 16000;\nconst AVATAR_AGENT_IDENTITY = 'lemonslice-avatar-agent';\nconst AVATAR_AGENT_NAME = 'lemonslice-avatar-agent';\n\n/**\n * Exception thrown when there are errors with the LemonSlice API.\n */\nexport class LemonSliceException extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'LemonSliceException';\n }\n}\n\n/**\n * Options for configuring an AvatarSession.\n */\nexport interface AvatarSessionOptions {\n /**\n * The ID of the LemonSlice agent to add to the session.\n * Either agentId or agentImageUrl must be provided.\n */\n agentId?: string | null;\n /**\n * The URL of the image to use as the agent's avatar.\n * Either agentId or agentImageUrl must be provided.\n */\n agentImageUrl?: string | null;\n /**\n * A prompt that subtly influences the avatar's movements and expressions.\n */\n agentPrompt?: string | null;\n /**\n * The idle timeout, in seconds. Defaults to 60 seconds.\n */\n idleTimeout?: number | null;\n /**\n * Additional payload fields to merge into the LemonSlice session creation request.\n */\n extraPayload?: Record<string, unknown> | null;\n /**\n * The LemonSlice API URL. Defaults to https://lemonslice.com/api/liveai/sessions\n * or LEMONSLICE_API_URL environment variable.\n */\n apiUrl?: string;\n /**\n * The LemonSlice API key. Can also be set via LEMONSLICE_API_KEY environment variable.\n */\n apiKey?: string;\n /**\n * The identity of the avatar participant in the room. Defaults to 'lemonslice-avatar-agent'.\n */\n avatarParticipantIdentity?: string;\n /**\n * The name of the avatar participant in the room. Defaults to 'lemonslice-avatar-agent'.\n */\n avatarParticipantName?: string;\n /**\n * Connection options for API requests.\n */\n connOptions?: APIConnectOptions;\n}\n\n/**\n * Options for starting an avatar session.\n */\nexport interface StartOptions {\n /**\n * LiveKit server URL. Falls back to LIVEKIT_URL environment variable.\n */\n livekitUrl?: string;\n /**\n * LiveKit API key. Falls back to LIVEKIT_API_KEY environment variable.\n */\n livekitApiKey?: string;\n /**\n * LiveKit API secret. Falls back to LIVEKIT_API_SECRET environment variable.\n */\n livekitApiSecret?: string;\n}\n\n/**\n * A LemonSlice avatar session.\n *\n * This class manages the connection between a LiveKit agent and a LemonSlice avatar,\n * routing agent audio output to the avatar for visual representation.\n *\n * @example\n * ```typescript\n * // Using an agent ID\n * const avatar = new AvatarSession({\n * agentId: 'your-agent-id',\n * apiKey: 'your-lemonslice-api-key',\n * });\n * await avatar.start(agentSession, room);\n *\n * // Using a custom avatar image\n * const avatar = new AvatarSession({\n * agentImageUrl: 'your-image-url',\n * apiKey: 'your-lemonslice-api-key',\n * });\n * await avatar.start(agentSession, room);\n * ```\n */\nexport class AvatarSession {\n private agentId: string | null;\n private agentImageUrl: string | null;\n private agentPrompt: string | null;\n private idleTimeout: number | null;\n private extraPayload: Record<string, unknown> | null;\n private apiUrl: string;\n private apiKey: string;\n private avatarParticipantIdentity: string;\n private avatarParticipantName: string;\n private connOptions: APIConnectOptions;\n\n #logger = log();\n\n /**\n * Creates a new AvatarSession.\n *\n * @param options - Configuration options for the avatar session\n * @throws LemonSliceException if invalid agentId or agentImageUrl is provided, or if LemonSlice API key is not set\n */\n constructor(options: AvatarSessionOptions = {}) {\n this.agentId = options.agentId ?? null;\n this.agentImageUrl = options.agentImageUrl ?? null;\n\n if (!this.agentId && !this.agentImageUrl) {\n throw new LemonSliceException('Missing agentId or agentImageUrl');\n }\n if (this.agentId && this.agentImageUrl) {\n throw new LemonSliceException('Only one of agentId or agentImageUrl can be provided');\n }\n\n this.agentPrompt = options.agentPrompt ?? null;\n this.idleTimeout = options.idleTimeout ?? null;\n this.extraPayload = options.extraPayload ?? null;\n\n this.apiUrl = options.apiUrl || process.env.LEMONSLICE_API_URL || DEFAULT_API_URL;\n this.apiKey = options.apiKey || process.env.LEMONSLICE_API_KEY || '';\n\n if (!this.apiKey) {\n throw new LemonSliceException(\n 'The api_key must be set either by passing apiKey to the client or ' +\n 'by setting the LEMONSLICE_API_KEY environment variable',\n );\n }\n\n this.avatarParticipantIdentity = options.avatarParticipantIdentity || AVATAR_AGENT_IDENTITY;\n this.avatarParticipantName = options.avatarParticipantName || AVATAR_AGENT_NAME;\n this.connOptions = options.connOptions || DEFAULT_API_CONNECT_OPTIONS;\n }\n\n /**\n * Starts the avatar session and connects it to the agent.\n *\n * This method:\n * 1. Creates a LiveKit token for the avatar participant\n * 2. Calls the LemonSlice API to start the avatar session\n * 3. Configures the agent's audio output to stream to the avatar\n *\n * @param agentSession - The agent session to connect to the avatar\n * @param room - The LiveKit room where the avatar will join\n * @param options - Optional LiveKit credentials (falls back to environment variables)\n * @throws LemonSliceException if LiveKit credentials are not available or if the avatar session fails to start\n * @returns The session ID of the LemonSlice session\n */\n async start(\n agentSession: voice.AgentSession,\n room: Room,\n options: StartOptions = {},\n ): Promise<string> {\n const livekitUrl = options.livekitUrl || process.env.LIVEKIT_URL;\n const livekitApiKey = options.livekitApiKey || process.env.LIVEKIT_API_KEY;\n const livekitApiSecret = options.livekitApiSecret || process.env.LIVEKIT_API_SECRET;\n\n if (!livekitUrl || !livekitApiKey || !livekitApiSecret) {\n throw new LemonSliceException(\n 'livekitUrl, livekitApiKey, and livekitApiSecret must be set ' +\n 'by arguments or environment variables',\n );\n }\n\n let localParticipantIdentity: string;\n try {\n const jobCtx = getJobContext();\n localParticipantIdentity = jobCtx.agent?.identity || '';\n if (!localParticipantIdentity && room.localParticipant) {\n localParticipantIdentity = room.localParticipant.identity;\n }\n } catch {\n if (!room.isConnected || !room.localParticipant) {\n throw new LemonSliceException('failed to get local participant identity');\n }\n localParticipantIdentity = room.localParticipant.identity;\n }\n\n if (!localParticipantIdentity) {\n throw new LemonSliceException('failed to get local participant identity');\n }\n\n const at = new AccessToken(livekitApiKey, livekitApiSecret, {\n identity: this.avatarParticipantIdentity,\n name: this.avatarParticipantName,\n });\n at.kind = 'agent';\n\n at.addGrant({\n roomJoin: true,\n room: room.name,\n } as VideoGrant);\n\n // allow the avatar agent to publish audio and video on behalf of your local agent\n at.attributes = {\n [ATTRIBUTE_PUBLISH_ON_BEHALF]: localParticipantIdentity,\n };\n\n const livekitToken = await at.toJwt();\n\n this.#logger.debug('starting avatar session');\n const sessionId = await this.startAgent(livekitUrl, livekitToken);\n\n agentSession.output.audio = new voice.DataStreamAudioOutput({\n room,\n destinationIdentity: this.avatarParticipantIdentity,\n sampleRate: SAMPLE_RATE,\n waitRemoteTrack: TrackKind.KIND_VIDEO,\n });\n\n return sessionId;\n }\n\n private async startAgent(livekitUrl: string, livekitToken: string): Promise<string> {\n for (let i = 0; i <= this.connOptions.maxRetry; i++) {\n try {\n const payload: Record<string, any> = {\n transport_type: 'livekit',\n properties: {\n livekit_url: livekitUrl,\n livekit_token: livekitToken,\n },\n };\n\n if (this.agentId) {\n payload.agent_id = this.agentId;\n }\n\n if (this.agentImageUrl) {\n payload.agent_image_url = this.agentImageUrl;\n }\n\n if (this.agentPrompt) {\n payload.agent_prompt = this.agentPrompt;\n }\n\n if (this.idleTimeout !== null) {\n payload.idle_timeout = this.idleTimeout;\n }\n\n if (this.extraPayload) {\n Object.assign(payload, this.extraPayload);\n }\n\n const response = await fetch(this.apiUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': this.apiKey,\n },\n body: JSON.stringify(payload),\n signal: AbortSignal.timeout(this.connOptions.timeoutMs),\n });\n\n if (!response.ok) {\n const text = await response.text();\n throw new APIStatusError({\n message: 'Server returned an error',\n options: { statusCode: response.status, body: { error: text } },\n });\n }\n const data = (await response.json()) as { session_id: string };\n return data.session_id;\n } catch (e) {\n if (e instanceof APIStatusError && !e.retryable) {\n throw e;\n }\n if (e instanceof APIConnectionError) {\n this.#logger.warn({ error: String(e) }, 'failed to call lemonslice api');\n } else {\n this.#logger.error({ error: e }, 'failed to call lemonslice api');\n }\n\n if (i <= this.connOptions.maxRetry - 1) {\n await new Promise((resolve) =>\n setTimeout(resolve, intervalForRetry(this.connOptions, i)),\n );\n }\n }\n }\n\n throw new APIConnectionError({\n message: 'Failed to start LemonSlice Avatar Session after all retries',\n });\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ var import_agents = require("@livekit/agents");
3
+ var import_vitest = require("vitest");
4
+ var import_avatar = require("./avatar.cjs");
5
+ (0, import_vitest.describe)("LemonSlice AvatarSession", () => {
6
+ (0, import_vitest.beforeEach)(() => {
7
+ (0, import_agents.initializeLogger)({ pretty: false });
8
+ global.fetch = import_vitest.vi.fn();
9
+ });
10
+ (0, import_vitest.afterEach)(() => {
11
+ import_vitest.vi.restoreAllMocks();
12
+ });
13
+ (0, import_vitest.it)("merges extraPayload into the session creation request body", async () => {
14
+ const mockFetch = import_vitest.vi.mocked(fetch);
15
+ mockFetch.mockResolvedValue({
16
+ ok: true,
17
+ json: async () => ({ session_id: "test-session-id" })
18
+ });
19
+ const avatar = new import_avatar.AvatarSession({
20
+ apiKey: "test-api-key",
21
+ agentImageUrl: "https://example.com/avatar.png",
22
+ extraPayload: {
23
+ aspect_ratio: "9x16"
24
+ }
25
+ });
26
+ await avatar.startAgent("wss://livekit.example.com", "livekit-token");
27
+ (0, import_vitest.expect)(mockFetch).toHaveBeenCalledTimes(1);
28
+ (0, import_vitest.expect)(mockFetch).toHaveBeenCalledWith(
29
+ "https://lemonslice.com/api/liveai/sessions",
30
+ import_vitest.expect.objectContaining({
31
+ method: "POST",
32
+ body: JSON.stringify({
33
+ transport_type: "livekit",
34
+ properties: {
35
+ livekit_url: "wss://livekit.example.com",
36
+ livekit_token: "livekit-token"
37
+ },
38
+ agent_image_url: "https://example.com/avatar.png",
39
+ aspect_ratio: "9x16"
40
+ })
41
+ })
42
+ );
43
+ });
44
+ (0, import_vitest.it)("keeps the request body unchanged when extraPayload is omitted", async () => {
45
+ var _a;
46
+ const mockFetch = import_vitest.vi.mocked(fetch);
47
+ mockFetch.mockResolvedValue({
48
+ ok: true,
49
+ json: async () => ({ session_id: "test-session-id" })
50
+ });
51
+ const avatar = new import_avatar.AvatarSession({
52
+ apiKey: "test-api-key",
53
+ agentImageUrl: "https://example.com/avatar.png"
54
+ });
55
+ await avatar.startAgent("wss://livekit.example.com", "livekit-token");
56
+ const requestInit = (_a = mockFetch.mock.calls[0]) == null ? void 0 : _a[1];
57
+ const body = JSON.parse(String(requestInit == null ? void 0 : requestInit.body));
58
+ (0, import_vitest.expect)(body).toEqual({
59
+ transport_type: "livekit",
60
+ properties: {
61
+ livekit_url: "wss://livekit.example.com",
62
+ livekit_token: "livekit-token"
63
+ },
64
+ agent_image_url: "https://example.com/avatar.png"
65
+ });
66
+ (0, import_vitest.expect)(body).not.toHaveProperty("aspect_ratio");
67
+ });
68
+ });
69
+ //# sourceMappingURL=avatar.test.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/avatar.test.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { initializeLogger } from '@livekit/agents';\nimport { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';\nimport { AvatarSession } from './avatar.js';\n\ndescribe('LemonSlice AvatarSession', () => {\n beforeEach(() => {\n initializeLogger({ pretty: false });\n global.fetch = vi.fn();\n });\n\n afterEach(() => {\n vi.restoreAllMocks();\n });\n\n it('merges extraPayload into the session creation request body', async () => {\n const mockFetch = vi.mocked(fetch);\n mockFetch.mockResolvedValue({\n ok: true,\n json: async () => ({ session_id: 'test-session-id' }),\n } as Response);\n\n const avatar = new AvatarSession({\n apiKey: 'test-api-key',\n agentImageUrl: 'https://example.com/avatar.png',\n extraPayload: {\n aspect_ratio: '9x16',\n },\n });\n\n await (\n avatar as unknown as {\n startAgent(livekitUrl: string, livekitToken: string): Promise<void>;\n }\n ).startAgent('wss://livekit.example.com', 'livekit-token');\n\n expect(mockFetch).toHaveBeenCalledTimes(1);\n expect(mockFetch).toHaveBeenCalledWith(\n 'https://lemonslice.com/api/liveai/sessions',\n expect.objectContaining({\n method: 'POST',\n body: JSON.stringify({\n transport_type: 'livekit',\n properties: {\n livekit_url: 'wss://livekit.example.com',\n livekit_token: 'livekit-token',\n },\n agent_image_url: 'https://example.com/avatar.png',\n aspect_ratio: '9x16',\n }),\n }),\n );\n });\n\n it('keeps the request body unchanged when extraPayload is omitted', async () => {\n const mockFetch = vi.mocked(fetch);\n mockFetch.mockResolvedValue({\n ok: true,\n json: async () => ({ session_id: 'test-session-id' }),\n } as Response);\n\n const avatar = new AvatarSession({\n apiKey: 'test-api-key',\n agentImageUrl: 'https://example.com/avatar.png',\n });\n\n await (\n avatar as unknown as {\n startAgent(livekitUrl: string, livekitToken: string): Promise<void>;\n }\n ).startAgent('wss://livekit.example.com', 'livekit-token');\n\n const requestInit = mockFetch.mock.calls[0]?.[1];\n const body = JSON.parse(String(requestInit?.body));\n\n expect(body).toEqual({\n transport_type: 'livekit',\n properties: {\n livekit_url: 'wss://livekit.example.com',\n livekit_token: 'livekit-token',\n },\n agent_image_url: 'https://example.com/avatar.png',\n });\n expect(body).not.toHaveProperty('aspect_ratio');\n });\n});\n"],"mappings":";AAGA,oBAAiC;AACjC,oBAAgE;AAChE,oBAA8B;AAAA,IAE9B,wBAAS,4BAA4B,MAAM;AACzC,gCAAW,MAAM;AACf,wCAAiB,EAAE,QAAQ,MAAM,CAAC;AAClC,WAAO,QAAQ,iBAAG,GAAG;AAAA,EACvB,CAAC;AAED,+BAAU,MAAM;AACd,qBAAG,gBAAgB;AAAA,EACrB,CAAC;AAED,wBAAG,8DAA8D,YAAY;AAC3E,UAAM,YAAY,iBAAG,OAAO,KAAK;AACjC,cAAU,kBAAkB;AAAA,MAC1B,IAAI;AAAA,MACJ,MAAM,aAAa,EAAE,YAAY,kBAAkB;AAAA,IACrD,CAAa;AAEb,UAAM,SAAS,IAAI,4BAAc;AAAA,MAC/B,QAAQ;AAAA,MACR,eAAe;AAAA,MACf,cAAc;AAAA,QACZ,cAAc;AAAA,MAChB;AAAA,IACF,CAAC;AAED,UACE,OAGA,WAAW,6BAA6B,eAAe;AAEzD,8BAAO,SAAS,EAAE,sBAAsB,CAAC;AACzC,8BAAO,SAAS,EAAE;AAAA,MAChB;AAAA,MACA,qBAAO,iBAAiB;AAAA,QACtB,QAAQ;AAAA,QACR,MAAM,KAAK,UAAU;AAAA,UACnB,gBAAgB;AAAA,UAChB,YAAY;AAAA,YACV,aAAa;AAAA,YACb,eAAe;AAAA,UACjB;AAAA,UACA,iBAAiB;AAAA,UACjB,cAAc;AAAA,QAChB,CAAC;AAAA,MACH,CAAC;AAAA,IACH;AAAA,EACF,CAAC;AAED,wBAAG,iEAAiE,YAAY;AAxDlF;AAyDI,UAAM,YAAY,iBAAG,OAAO,KAAK;AACjC,cAAU,kBAAkB;AAAA,MAC1B,IAAI;AAAA,MACJ,MAAM,aAAa,EAAE,YAAY,kBAAkB;AAAA,IACrD,CAAa;AAEb,UAAM,SAAS,IAAI,4BAAc;AAAA,MAC/B,QAAQ;AAAA,MACR,eAAe;AAAA,IACjB,CAAC;AAED,UACE,OAGA,WAAW,6BAA6B,eAAe;AAEzD,UAAM,eAAc,eAAU,KAAK,MAAM,CAAC,MAAtB,mBAA0B;AAC9C,UAAM,OAAO,KAAK,MAAM,OAAO,2CAAa,IAAI,CAAC;AAEjD,8BAAO,IAAI,EAAE,QAAQ;AAAA,MACnB,gBAAgB;AAAA,MAChB,YAAY;AAAA,QACV,aAAa;AAAA,QACb,eAAe;AAAA,MACjB;AAAA,MACA,iBAAiB;AAAA,IACnB,CAAC;AACD,8BAAO,IAAI,EAAE,IAAI,eAAe,cAAc;AAAA,EAChD,CAAC;AACH,CAAC;","names":[]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=avatar.test.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=avatar.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"avatar.test.d.ts","sourceRoot":"","sources":["../src/avatar.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,68 @@
1
+ import { initializeLogger } from "@livekit/agents";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { AvatarSession } from "./avatar.js";
4
+ describe("LemonSlice AvatarSession", () => {
5
+ beforeEach(() => {
6
+ initializeLogger({ pretty: false });
7
+ global.fetch = vi.fn();
8
+ });
9
+ afterEach(() => {
10
+ vi.restoreAllMocks();
11
+ });
12
+ it("merges extraPayload into the session creation request body", async () => {
13
+ const mockFetch = vi.mocked(fetch);
14
+ mockFetch.mockResolvedValue({
15
+ ok: true,
16
+ json: async () => ({ session_id: "test-session-id" })
17
+ });
18
+ const avatar = new AvatarSession({
19
+ apiKey: "test-api-key",
20
+ agentImageUrl: "https://example.com/avatar.png",
21
+ extraPayload: {
22
+ aspect_ratio: "9x16"
23
+ }
24
+ });
25
+ await avatar.startAgent("wss://livekit.example.com", "livekit-token");
26
+ expect(mockFetch).toHaveBeenCalledTimes(1);
27
+ expect(mockFetch).toHaveBeenCalledWith(
28
+ "https://lemonslice.com/api/liveai/sessions",
29
+ expect.objectContaining({
30
+ method: "POST",
31
+ body: JSON.stringify({
32
+ transport_type: "livekit",
33
+ properties: {
34
+ livekit_url: "wss://livekit.example.com",
35
+ livekit_token: "livekit-token"
36
+ },
37
+ agent_image_url: "https://example.com/avatar.png",
38
+ aspect_ratio: "9x16"
39
+ })
40
+ })
41
+ );
42
+ });
43
+ it("keeps the request body unchanged when extraPayload is omitted", async () => {
44
+ var _a;
45
+ const mockFetch = vi.mocked(fetch);
46
+ mockFetch.mockResolvedValue({
47
+ ok: true,
48
+ json: async () => ({ session_id: "test-session-id" })
49
+ });
50
+ const avatar = new AvatarSession({
51
+ apiKey: "test-api-key",
52
+ agentImageUrl: "https://example.com/avatar.png"
53
+ });
54
+ await avatar.startAgent("wss://livekit.example.com", "livekit-token");
55
+ const requestInit = (_a = mockFetch.mock.calls[0]) == null ? void 0 : _a[1];
56
+ const body = JSON.parse(String(requestInit == null ? void 0 : requestInit.body));
57
+ expect(body).toEqual({
58
+ transport_type: "livekit",
59
+ properties: {
60
+ livekit_url: "wss://livekit.example.com",
61
+ livekit_token: "livekit-token"
62
+ },
63
+ agent_image_url: "https://example.com/avatar.png"
64
+ });
65
+ expect(body).not.toHaveProperty("aspect_ratio");
66
+ });
67
+ });
68
+ //# sourceMappingURL=avatar.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/avatar.test.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { initializeLogger } from '@livekit/agents';\nimport { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';\nimport { AvatarSession } from './avatar.js';\n\ndescribe('LemonSlice AvatarSession', () => {\n beforeEach(() => {\n initializeLogger({ pretty: false });\n global.fetch = vi.fn();\n });\n\n afterEach(() => {\n vi.restoreAllMocks();\n });\n\n it('merges extraPayload into the session creation request body', async () => {\n const mockFetch = vi.mocked(fetch);\n mockFetch.mockResolvedValue({\n ok: true,\n json: async () => ({ session_id: 'test-session-id' }),\n } as Response);\n\n const avatar = new AvatarSession({\n apiKey: 'test-api-key',\n agentImageUrl: 'https://example.com/avatar.png',\n extraPayload: {\n aspect_ratio: '9x16',\n },\n });\n\n await (\n avatar as unknown as {\n startAgent(livekitUrl: string, livekitToken: string): Promise<void>;\n }\n ).startAgent('wss://livekit.example.com', 'livekit-token');\n\n expect(mockFetch).toHaveBeenCalledTimes(1);\n expect(mockFetch).toHaveBeenCalledWith(\n 'https://lemonslice.com/api/liveai/sessions',\n expect.objectContaining({\n method: 'POST',\n body: JSON.stringify({\n transport_type: 'livekit',\n properties: {\n livekit_url: 'wss://livekit.example.com',\n livekit_token: 'livekit-token',\n },\n agent_image_url: 'https://example.com/avatar.png',\n aspect_ratio: '9x16',\n }),\n }),\n );\n });\n\n it('keeps the request body unchanged when extraPayload is omitted', async () => {\n const mockFetch = vi.mocked(fetch);\n mockFetch.mockResolvedValue({\n ok: true,\n json: async () => ({ session_id: 'test-session-id' }),\n } as Response);\n\n const avatar = new AvatarSession({\n apiKey: 'test-api-key',\n agentImageUrl: 'https://example.com/avatar.png',\n });\n\n await (\n avatar as unknown as {\n startAgent(livekitUrl: string, livekitToken: string): Promise<void>;\n }\n ).startAgent('wss://livekit.example.com', 'livekit-token');\n\n const requestInit = mockFetch.mock.calls[0]?.[1];\n const body = JSON.parse(String(requestInit?.body));\n\n expect(body).toEqual({\n transport_type: 'livekit',\n properties: {\n livekit_url: 'wss://livekit.example.com',\n livekit_token: 'livekit-token',\n },\n agent_image_url: 'https://example.com/avatar.png',\n });\n expect(body).not.toHaveProperty('aspect_ratio');\n });\n});\n"],"mappings":"AAGA,SAAS,wBAAwB;AACjC,SAAS,WAAW,YAAY,UAAU,QAAQ,IAAI,UAAU;AAChE,SAAS,qBAAqB;AAE9B,SAAS,4BAA4B,MAAM;AACzC,aAAW,MAAM;AACf,qBAAiB,EAAE,QAAQ,MAAM,CAAC;AAClC,WAAO,QAAQ,GAAG,GAAG;AAAA,EACvB,CAAC;AAED,YAAU,MAAM;AACd,OAAG,gBAAgB;AAAA,EACrB,CAAC;AAED,KAAG,8DAA8D,YAAY;AAC3E,UAAM,YAAY,GAAG,OAAO,KAAK;AACjC,cAAU,kBAAkB;AAAA,MAC1B,IAAI;AAAA,MACJ,MAAM,aAAa,EAAE,YAAY,kBAAkB;AAAA,IACrD,CAAa;AAEb,UAAM,SAAS,IAAI,cAAc;AAAA,MAC/B,QAAQ;AAAA,MACR,eAAe;AAAA,MACf,cAAc;AAAA,QACZ,cAAc;AAAA,MAChB;AAAA,IACF,CAAC;AAED,UACE,OAGA,WAAW,6BAA6B,eAAe;AAEzD,WAAO,SAAS,EAAE,sBAAsB,CAAC;AACzC,WAAO,SAAS,EAAE;AAAA,MAChB;AAAA,MACA,OAAO,iBAAiB;AAAA,QACtB,QAAQ;AAAA,QACR,MAAM,KAAK,UAAU;AAAA,UACnB,gBAAgB;AAAA,UAChB,YAAY;AAAA,YACV,aAAa;AAAA,YACb,eAAe;AAAA,UACjB;AAAA,UACA,iBAAiB;AAAA,UACjB,cAAc;AAAA,QAChB,CAAC;AAAA,MACH,CAAC;AAAA,IACH;AAAA,EACF,CAAC;AAED,KAAG,iEAAiE,YAAY;AAxDlF;AAyDI,UAAM,YAAY,GAAG,OAAO,KAAK;AACjC,cAAU,kBAAkB;AAAA,MAC1B,IAAI;AAAA,MACJ,MAAM,aAAa,EAAE,YAAY,kBAAkB;AAAA,IACrD,CAAa;AAEb,UAAM,SAAS,IAAI,cAAc;AAAA,MAC/B,QAAQ;AAAA,MACR,eAAe;AAAA,IACjB,CAAC;AAED,UACE,OAGA,WAAW,6BAA6B,eAAe;AAEzD,UAAM,eAAc,eAAU,KAAK,MAAM,CAAC,MAAtB,mBAA0B;AAC9C,UAAM,OAAO,KAAK,MAAM,OAAO,2CAAa,IAAI,CAAC;AAEjD,WAAO,IAAI,EAAE,QAAQ;AAAA,MACnB,gBAAgB;AAAA,MAChB,YAAY;AAAA,QACV,aAAa;AAAA,QACb,eAAe;AAAA,MACjB;AAAA,MACA,iBAAiB;AAAA,IACnB,CAAC;AACD,WAAO,IAAI,EAAE,IAAI,eAAe,cAAc;AAAA,EAChD,CAAC;AACH,CAAC;","names":[]}
package/dist/index.cjs CHANGED
@@ -21,7 +21,7 @@ class LemonSlicePlugin extends import_agents.Plugin {
21
21
  constructor() {
22
22
  super({
23
23
  title: "lemonslice",
24
- version: "1.0.50",
24
+ version: "1.0.51",
25
25
  package: "@livekit/agents-plugin-lemonslice"
26
26
  });
27
27
  }
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ class LemonSlicePlugin extends Plugin {
4
4
  constructor() {
5
5
  super({
6
6
  title: "lemonslice",
7
- version: "1.0.50",
7
+ version: "1.0.51",
8
8
  package: "@livekit/agents-plugin-lemonslice"
9
9
  });
10
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livekit/agents-plugin-lemonslice",
3
- "version": "1.0.50",
3
+ "version": "1.0.51",
4
4
  "description": "LemonSlice avatar plugin for LiveKit Node Agents",
5
5
  "main": "dist/index.js",
6
6
  "require": "dist/index.cjs",
@@ -30,14 +30,14 @@
30
30
  "pino": "^8.19.0",
31
31
  "tsup": "^8.3.5",
32
32
  "typescript": "^5.0.0",
33
- "@livekit/agents": "1.0.50"
33
+ "@livekit/agents": "1.0.51"
34
34
  },
35
35
  "dependencies": {
36
36
  "livekit-server-sdk": "^2.13.3"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@livekit/rtc-node": "^0.13.24",
40
- "@livekit/agents": "1.0.50"
40
+ "@livekit/agents": "1.0.51"
41
41
  },
42
42
  "scripts": {
43
43
  "build": "tsup --onSuccess \"pnpm build:types\"",
@@ -0,0 +1,88 @@
1
+ // SPDX-FileCopyrightText: 2026 LiveKit, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ import { initializeLogger } from '@livekit/agents';
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6
+ import { AvatarSession } from './avatar.js';
7
+
8
+ describe('LemonSlice AvatarSession', () => {
9
+ beforeEach(() => {
10
+ initializeLogger({ pretty: false });
11
+ global.fetch = vi.fn();
12
+ });
13
+
14
+ afterEach(() => {
15
+ vi.restoreAllMocks();
16
+ });
17
+
18
+ it('merges extraPayload into the session creation request body', async () => {
19
+ const mockFetch = vi.mocked(fetch);
20
+ mockFetch.mockResolvedValue({
21
+ ok: true,
22
+ json: async () => ({ session_id: 'test-session-id' }),
23
+ } as Response);
24
+
25
+ const avatar = new AvatarSession({
26
+ apiKey: 'test-api-key',
27
+ agentImageUrl: 'https://example.com/avatar.png',
28
+ extraPayload: {
29
+ aspect_ratio: '9x16',
30
+ },
31
+ });
32
+
33
+ await (
34
+ avatar as unknown as {
35
+ startAgent(livekitUrl: string, livekitToken: string): Promise<void>;
36
+ }
37
+ ).startAgent('wss://livekit.example.com', 'livekit-token');
38
+
39
+ expect(mockFetch).toHaveBeenCalledTimes(1);
40
+ expect(mockFetch).toHaveBeenCalledWith(
41
+ 'https://lemonslice.com/api/liveai/sessions',
42
+ expect.objectContaining({
43
+ method: 'POST',
44
+ body: JSON.stringify({
45
+ transport_type: 'livekit',
46
+ properties: {
47
+ livekit_url: 'wss://livekit.example.com',
48
+ livekit_token: 'livekit-token',
49
+ },
50
+ agent_image_url: 'https://example.com/avatar.png',
51
+ aspect_ratio: '9x16',
52
+ }),
53
+ }),
54
+ );
55
+ });
56
+
57
+ it('keeps the request body unchanged when extraPayload is omitted', async () => {
58
+ const mockFetch = vi.mocked(fetch);
59
+ mockFetch.mockResolvedValue({
60
+ ok: true,
61
+ json: async () => ({ session_id: 'test-session-id' }),
62
+ } as Response);
63
+
64
+ const avatar = new AvatarSession({
65
+ apiKey: 'test-api-key',
66
+ agentImageUrl: 'https://example.com/avatar.png',
67
+ });
68
+
69
+ await (
70
+ avatar as unknown as {
71
+ startAgent(livekitUrl: string, livekitToken: string): Promise<void>;
72
+ }
73
+ ).startAgent('wss://livekit.example.com', 'livekit-token');
74
+
75
+ const requestInit = mockFetch.mock.calls[0]?.[1];
76
+ const body = JSON.parse(String(requestInit?.body));
77
+
78
+ expect(body).toEqual({
79
+ transport_type: 'livekit',
80
+ properties: {
81
+ livekit_url: 'wss://livekit.example.com',
82
+ livekit_token: 'livekit-token',
83
+ },
84
+ agent_image_url: 'https://example.com/avatar.png',
85
+ });
86
+ expect(body).not.toHaveProperty('aspect_ratio');
87
+ });
88
+ });
package/src/avatar.ts CHANGED
@@ -54,6 +54,10 @@ export interface AvatarSessionOptions {
54
54
  * The idle timeout, in seconds. Defaults to 60 seconds.
55
55
  */
56
56
  idleTimeout?: number | null;
57
+ /**
58
+ * Additional payload fields to merge into the LemonSlice session creation request.
59
+ */
60
+ extraPayload?: Record<string, unknown> | null;
57
61
  /**
58
62
  * The LemonSlice API URL. Defaults to https://lemonslice.com/api/liveai/sessions
59
63
  * or LEMONSLICE_API_URL environment variable.
@@ -123,6 +127,7 @@ export class AvatarSession {
123
127
  private agentImageUrl: string | null;
124
128
  private agentPrompt: string | null;
125
129
  private idleTimeout: number | null;
130
+ private extraPayload: Record<string, unknown> | null;
126
131
  private apiUrl: string;
127
132
  private apiKey: string;
128
133
  private avatarParticipantIdentity: string;
@@ -150,6 +155,7 @@ export class AvatarSession {
150
155
 
151
156
  this.agentPrompt = options.agentPrompt ?? null;
152
157
  this.idleTimeout = options.idleTimeout ?? null;
158
+ this.extraPayload = options.extraPayload ?? null;
153
159
 
154
160
  this.apiUrl = options.apiUrl || process.env.LEMONSLICE_API_URL || DEFAULT_API_URL;
155
161
  this.apiKey = options.apiKey || process.env.LEMONSLICE_API_KEY || '';
@@ -178,12 +184,13 @@ export class AvatarSession {
178
184
  * @param room - The LiveKit room where the avatar will join
179
185
  * @param options - Optional LiveKit credentials (falls back to environment variables)
180
186
  * @throws LemonSliceException if LiveKit credentials are not available or if the avatar session fails to start
187
+ * @returns The session ID of the LemonSlice session
181
188
  */
182
189
  async start(
183
190
  agentSession: voice.AgentSession,
184
191
  room: Room,
185
192
  options: StartOptions = {},
186
- ): Promise<void> {
193
+ ): Promise<string> {
187
194
  const livekitUrl = options.livekitUrl || process.env.LIVEKIT_URL;
188
195
  const livekitApiKey = options.livekitApiKey || process.env.LIVEKIT_API_KEY;
189
196
  const livekitApiSecret = options.livekitApiSecret || process.env.LIVEKIT_API_SECRET;
@@ -232,7 +239,7 @@ export class AvatarSession {
232
239
  const livekitToken = await at.toJwt();
233
240
 
234
241
  this.#logger.debug('starting avatar session');
235
- await this.startAgent(livekitUrl, livekitToken);
242
+ const sessionId = await this.startAgent(livekitUrl, livekitToken);
236
243
 
237
244
  agentSession.output.audio = new voice.DataStreamAudioOutput({
238
245
  room,
@@ -240,9 +247,11 @@ export class AvatarSession {
240
247
  sampleRate: SAMPLE_RATE,
241
248
  waitRemoteTrack: TrackKind.KIND_VIDEO,
242
249
  });
250
+
251
+ return sessionId;
243
252
  }
244
253
 
245
- private async startAgent(livekitUrl: string, livekitToken: string): Promise<void> {
254
+ private async startAgent(livekitUrl: string, livekitToken: string): Promise<string> {
246
255
  for (let i = 0; i <= this.connOptions.maxRetry; i++) {
247
256
  try {
248
257
  const payload: Record<string, any> = {
@@ -269,6 +278,10 @@ export class AvatarSession {
269
278
  payload.idle_timeout = this.idleTimeout;
270
279
  }
271
280
 
281
+ if (this.extraPayload) {
282
+ Object.assign(payload, this.extraPayload);
283
+ }
284
+
272
285
  const response = await fetch(this.apiUrl, {
273
286
  method: 'POST',
274
287
  headers: {
@@ -286,7 +299,8 @@ export class AvatarSession {
286
299
  options: { statusCode: response.status, body: { error: text } },
287
300
  });
288
301
  }
289
- return;
302
+ const data = (await response.json()) as { session_id: string };
303
+ return data.session_id;
290
304
  } catch (e) {
291
305
  if (e instanceof APIStatusError && !e.retryable) {
292
306
  throw e;