@livekit/agents 1.0.17 → 1.0.19

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.
Files changed (216) hide show
  1. package/dist/index.cjs +3 -0
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +2 -1
  4. package/dist/index.d.ts +2 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +2 -0
  7. package/dist/index.js.map +1 -1
  8. package/dist/inference/api_protos.d.cts +12 -12
  9. package/dist/inference/api_protos.d.ts +12 -12
  10. package/dist/inference/llm.cjs +35 -13
  11. package/dist/inference/llm.cjs.map +1 -1
  12. package/dist/inference/llm.d.cts +10 -5
  13. package/dist/inference/llm.d.ts +10 -5
  14. package/dist/inference/llm.d.ts.map +1 -1
  15. package/dist/inference/llm.js +35 -13
  16. package/dist/inference/llm.js.map +1 -1
  17. package/dist/inference/tts.cjs +1 -1
  18. package/dist/inference/tts.cjs.map +1 -1
  19. package/dist/inference/tts.js +1 -1
  20. package/dist/inference/tts.js.map +1 -1
  21. package/dist/ipc/job_proc_lazy_main.cjs +6 -2
  22. package/dist/ipc/job_proc_lazy_main.cjs.map +1 -1
  23. package/dist/ipc/job_proc_lazy_main.js +6 -2
  24. package/dist/ipc/job_proc_lazy_main.js.map +1 -1
  25. package/dist/job.cjs +31 -0
  26. package/dist/job.cjs.map +1 -1
  27. package/dist/job.d.cts +6 -0
  28. package/dist/job.d.ts +6 -0
  29. package/dist/job.d.ts.map +1 -1
  30. package/dist/job.js +31 -0
  31. package/dist/job.js.map +1 -1
  32. package/dist/llm/chat_context.cjs +33 -0
  33. package/dist/llm/chat_context.cjs.map +1 -1
  34. package/dist/llm/chat_context.d.cts +22 -2
  35. package/dist/llm/chat_context.d.ts +22 -2
  36. package/dist/llm/chat_context.d.ts.map +1 -1
  37. package/dist/llm/chat_context.js +32 -0
  38. package/dist/llm/chat_context.js.map +1 -1
  39. package/dist/llm/index.cjs +2 -0
  40. package/dist/llm/index.cjs.map +1 -1
  41. package/dist/llm/index.d.cts +1 -1
  42. package/dist/llm/index.d.ts +1 -1
  43. package/dist/llm/index.d.ts.map +1 -1
  44. package/dist/llm/index.js +2 -0
  45. package/dist/llm/index.js.map +1 -1
  46. package/dist/llm/llm.cjs.map +1 -1
  47. package/dist/llm/llm.d.cts +1 -1
  48. package/dist/llm/llm.d.ts +1 -1
  49. package/dist/llm/llm.d.ts.map +1 -1
  50. package/dist/llm/llm.js.map +1 -1
  51. package/dist/llm/provider_format/google.cjs.map +1 -1
  52. package/dist/llm/provider_format/google.d.cts +1 -1
  53. package/dist/llm/provider_format/google.d.ts +1 -1
  54. package/dist/llm/provider_format/google.d.ts.map +1 -1
  55. package/dist/llm/provider_format/google.js.map +1 -1
  56. package/dist/llm/provider_format/google.test.cjs +48 -0
  57. package/dist/llm/provider_format/google.test.cjs.map +1 -1
  58. package/dist/llm/provider_format/google.test.js +54 -1
  59. package/dist/llm/provider_format/google.test.js.map +1 -1
  60. package/dist/llm/provider_format/index.d.cts +1 -1
  61. package/dist/llm/provider_format/index.d.ts +1 -1
  62. package/dist/llm/provider_format/index.d.ts.map +1 -1
  63. package/dist/llm/provider_format/openai.cjs +1 -2
  64. package/dist/llm/provider_format/openai.cjs.map +1 -1
  65. package/dist/llm/provider_format/openai.js +1 -2
  66. package/dist/llm/provider_format/openai.js.map +1 -1
  67. package/dist/llm/provider_format/openai.test.cjs +32 -0
  68. package/dist/llm/provider_format/openai.test.cjs.map +1 -1
  69. package/dist/llm/provider_format/openai.test.js +38 -1
  70. package/dist/llm/provider_format/openai.test.js.map +1 -1
  71. package/dist/llm/realtime.cjs.map +1 -1
  72. package/dist/llm/realtime.d.cts +4 -0
  73. package/dist/llm/realtime.d.ts +4 -0
  74. package/dist/llm/realtime.d.ts.map +1 -1
  75. package/dist/llm/realtime.js.map +1 -1
  76. package/dist/llm/utils.cjs +2 -2
  77. package/dist/llm/utils.cjs.map +1 -1
  78. package/dist/llm/utils.d.cts +1 -1
  79. package/dist/llm/utils.d.ts +1 -1
  80. package/dist/llm/utils.d.ts.map +1 -1
  81. package/dist/llm/utils.js +2 -2
  82. package/dist/llm/utils.js.map +1 -1
  83. package/dist/llm/zod-utils.cjs +6 -3
  84. package/dist/llm/zod-utils.cjs.map +1 -1
  85. package/dist/llm/zod-utils.d.cts +1 -1
  86. package/dist/llm/zod-utils.d.ts +1 -1
  87. package/dist/llm/zod-utils.d.ts.map +1 -1
  88. package/dist/llm/zod-utils.js +6 -3
  89. package/dist/llm/zod-utils.js.map +1 -1
  90. package/dist/llm/zod-utils.test.cjs +83 -0
  91. package/dist/llm/zod-utils.test.cjs.map +1 -1
  92. package/dist/llm/zod-utils.test.js +83 -0
  93. package/dist/llm/zod-utils.test.js.map +1 -1
  94. package/dist/log.cjs.map +1 -1
  95. package/dist/log.d.ts.map +1 -1
  96. package/dist/log.js.map +1 -1
  97. package/dist/telemetry/index.cjs +51 -0
  98. package/dist/telemetry/index.cjs.map +1 -0
  99. package/dist/telemetry/index.d.cts +4 -0
  100. package/dist/telemetry/index.d.ts +4 -0
  101. package/dist/telemetry/index.d.ts.map +1 -0
  102. package/dist/telemetry/index.js +12 -0
  103. package/dist/telemetry/index.js.map +1 -0
  104. package/dist/telemetry/trace_types.cjs +191 -0
  105. package/dist/telemetry/trace_types.cjs.map +1 -0
  106. package/dist/telemetry/trace_types.d.cts +56 -0
  107. package/dist/telemetry/trace_types.d.ts +56 -0
  108. package/dist/telemetry/trace_types.d.ts.map +1 -0
  109. package/dist/telemetry/trace_types.js +113 -0
  110. package/dist/telemetry/trace_types.js.map +1 -0
  111. package/dist/telemetry/traces.cjs +196 -0
  112. package/dist/telemetry/traces.cjs.map +1 -0
  113. package/dist/telemetry/traces.d.cts +97 -0
  114. package/dist/telemetry/traces.d.ts +97 -0
  115. package/dist/telemetry/traces.d.ts.map +1 -0
  116. package/dist/telemetry/traces.js +173 -0
  117. package/dist/telemetry/traces.js.map +1 -0
  118. package/dist/telemetry/utils.cjs +86 -0
  119. package/dist/telemetry/utils.cjs.map +1 -0
  120. package/dist/telemetry/utils.d.cts +5 -0
  121. package/dist/telemetry/utils.d.ts +5 -0
  122. package/dist/telemetry/utils.d.ts.map +1 -0
  123. package/dist/telemetry/utils.js +51 -0
  124. package/dist/telemetry/utils.js.map +1 -0
  125. package/dist/tts/tts.cjs.map +1 -1
  126. package/dist/tts/tts.d.ts.map +1 -1
  127. package/dist/tts/tts.js.map +1 -1
  128. package/dist/utils.cjs.map +1 -1
  129. package/dist/utils.d.cts +7 -0
  130. package/dist/utils.d.ts +7 -0
  131. package/dist/utils.d.ts.map +1 -1
  132. package/dist/utils.js.map +1 -1
  133. package/dist/voice/agent.cjs +15 -0
  134. package/dist/voice/agent.cjs.map +1 -1
  135. package/dist/voice/agent.d.cts +4 -1
  136. package/dist/voice/agent.d.ts +4 -1
  137. package/dist/voice/agent.d.ts.map +1 -1
  138. package/dist/voice/agent.js +15 -0
  139. package/dist/voice/agent.js.map +1 -1
  140. package/dist/voice/agent_activity.cjs +71 -20
  141. package/dist/voice/agent_activity.cjs.map +1 -1
  142. package/dist/voice/agent_activity.d.ts.map +1 -1
  143. package/dist/voice/agent_activity.js +71 -20
  144. package/dist/voice/agent_activity.js.map +1 -1
  145. package/dist/voice/agent_session.cjs +69 -2
  146. package/dist/voice/agent_session.cjs.map +1 -1
  147. package/dist/voice/agent_session.d.cts +11 -2
  148. package/dist/voice/agent_session.d.ts +11 -2
  149. package/dist/voice/agent_session.d.ts.map +1 -1
  150. package/dist/voice/agent_session.js +70 -3
  151. package/dist/voice/agent_session.js.map +1 -1
  152. package/dist/voice/audio_recognition.cjs.map +1 -1
  153. package/dist/voice/audio_recognition.d.ts.map +1 -1
  154. package/dist/voice/audio_recognition.js.map +1 -1
  155. package/dist/voice/generation.cjs.map +1 -1
  156. package/dist/voice/generation.d.ts.map +1 -1
  157. package/dist/voice/generation.js.map +1 -1
  158. package/dist/voice/index.cjs +2 -0
  159. package/dist/voice/index.cjs.map +1 -1
  160. package/dist/voice/index.d.cts +1 -0
  161. package/dist/voice/index.d.ts +1 -0
  162. package/dist/voice/index.d.ts.map +1 -1
  163. package/dist/voice/index.js +1 -0
  164. package/dist/voice/index.js.map +1 -1
  165. package/dist/voice/interruption_detection.test.cjs +114 -0
  166. package/dist/voice/interruption_detection.test.cjs.map +1 -0
  167. package/dist/voice/interruption_detection.test.js +113 -0
  168. package/dist/voice/interruption_detection.test.js.map +1 -0
  169. package/dist/voice/report.cjs +69 -0
  170. package/dist/voice/report.cjs.map +1 -0
  171. package/dist/voice/report.d.cts +26 -0
  172. package/dist/voice/report.d.ts +26 -0
  173. package/dist/voice/report.d.ts.map +1 -0
  174. package/dist/voice/report.js +44 -0
  175. package/dist/voice/report.js.map +1 -0
  176. package/dist/voice/room_io/room_io.cjs +3 -0
  177. package/dist/voice/room_io/room_io.cjs.map +1 -1
  178. package/dist/voice/room_io/room_io.d.cts +1 -0
  179. package/dist/voice/room_io/room_io.d.ts +1 -0
  180. package/dist/voice/room_io/room_io.d.ts.map +1 -1
  181. package/dist/voice/room_io/room_io.js +3 -0
  182. package/dist/voice/room_io/room_io.js.map +1 -1
  183. package/package.json +12 -5
  184. package/src/index.ts +2 -1
  185. package/src/inference/llm.ts +53 -21
  186. package/src/inference/tts.ts +1 -1
  187. package/src/ipc/job_proc_lazy_main.ts +10 -2
  188. package/src/job.ts +48 -0
  189. package/src/llm/__snapshots__/zod-utils.test.ts.snap +218 -0
  190. package/src/llm/chat_context.ts +53 -1
  191. package/src/llm/index.ts +1 -0
  192. package/src/llm/llm.ts +3 -1
  193. package/src/llm/provider_format/google.test.ts +72 -1
  194. package/src/llm/provider_format/google.ts +4 -4
  195. package/src/llm/provider_format/openai.test.ts +55 -1
  196. package/src/llm/provider_format/openai.ts +3 -2
  197. package/src/llm/realtime.ts +8 -1
  198. package/src/llm/utils.ts +7 -2
  199. package/src/llm/zod-utils.test.ts +101 -0
  200. package/src/llm/zod-utils.ts +12 -3
  201. package/src/log.ts +1 -0
  202. package/src/telemetry/index.ts +10 -0
  203. package/src/telemetry/trace_types.ts +88 -0
  204. package/src/telemetry/traces.ts +266 -0
  205. package/src/telemetry/utils.ts +61 -0
  206. package/src/tts/tts.ts +4 -0
  207. package/src/utils.ts +17 -0
  208. package/src/voice/agent.ts +22 -0
  209. package/src/voice/agent_activity.ts +102 -24
  210. package/src/voice/agent_session.ts +98 -1
  211. package/src/voice/audio_recognition.ts +2 -0
  212. package/src/voice/generation.ts +3 -0
  213. package/src/voice/index.ts +1 -0
  214. package/src/voice/interruption_detection.test.ts +151 -0
  215. package/src/voice/report.ts +77 -0
  216. package/src/voice/room_io/room_io.ts +4 -0
@@ -4,7 +4,12 @@
4
4
  import { VideoBufferType, VideoFrame } from '@livekit/rtc-node';
5
5
  import { beforeEach, describe, expect, it, vi } from 'vitest';
6
6
  import { initializeLogger } from '../../log.js';
7
- import { ChatContext, FunctionCall, FunctionCallOutput } from '../chat_context.js';
7
+ import {
8
+ AgentHandoffItem,
9
+ ChatContext,
10
+ FunctionCall,
11
+ FunctionCallOutput,
12
+ } from '../chat_context.js';
8
13
  import { serializeImage } from '../utils.js';
9
14
  import { toChatCtx } from './google.js';
10
15
 
@@ -769,4 +774,70 @@ describe('Google Provider Format - toChatCtx', () => {
769
774
  ]);
770
775
  expect(formatData.systemMessages).toBeNull();
771
776
  });
777
+
778
+ it('should filter out agent handoff items', async () => {
779
+ const ctx = ChatContext.empty();
780
+
781
+ ctx.addMessage({ role: 'user', content: 'Hello' });
782
+
783
+ // Insert an agent handoff item
784
+ const handoff = new AgentHandoffItem({
785
+ oldAgentId: 'agent_1',
786
+ newAgentId: 'agent_2',
787
+ });
788
+ ctx.insert(handoff);
789
+
790
+ ctx.addMessage({ role: 'assistant', content: 'Hi there!' });
791
+
792
+ const [result, formatData] = await toChatCtx(ctx, false);
793
+
794
+ // Agent handoff should be filtered out, only messages should remain
795
+ expect(result).toEqual([
796
+ {
797
+ role: 'user',
798
+ parts: [{ text: 'Hello' }],
799
+ },
800
+ {
801
+ role: 'model',
802
+ parts: [{ text: 'Hi there!' }],
803
+ },
804
+ ]);
805
+ expect(formatData.systemMessages).toBeNull();
806
+ });
807
+
808
+ it('should handle multiple agent handoffs without errors', async () => {
809
+ const ctx = ChatContext.empty();
810
+
811
+ ctx.addMessage({ role: 'user', content: 'Start' });
812
+
813
+ // Multiple handoffs
814
+ ctx.insert(new AgentHandoffItem({ oldAgentId: undefined, newAgentId: 'agent_1' }));
815
+ ctx.addMessage({ role: 'assistant', content: 'Response from agent 1' });
816
+
817
+ ctx.insert(new AgentHandoffItem({ oldAgentId: 'agent_1', newAgentId: 'agent_2' }));
818
+ ctx.addMessage({ role: 'assistant', content: 'Response from agent 2' });
819
+
820
+ ctx.insert(new AgentHandoffItem({ oldAgentId: 'agent_2', newAgentId: 'agent_3' }));
821
+ ctx.addMessage({ role: 'assistant', content: 'Response from agent 3' });
822
+
823
+ const [result, formatData] = await toChatCtx(ctx, false);
824
+
825
+ // All handoffs should be filtered out
826
+ // Note: Google provider groups consecutive messages by the same role
827
+ expect(result).toEqual([
828
+ {
829
+ role: 'user',
830
+ parts: [{ text: 'Start' }],
831
+ },
832
+ {
833
+ role: 'model',
834
+ parts: [
835
+ { text: 'Response from agent 1' },
836
+ { text: 'Response from agent 2' },
837
+ { text: 'Response from agent 3' },
838
+ ],
839
+ },
840
+ ]);
841
+ expect(formatData.systemMessages).toBeNull();
842
+ });
772
843
  });
@@ -12,11 +12,11 @@ export interface GoogleFormatData {
12
12
  export async function toChatCtx(
13
13
  chatCtx: ChatContext,
14
14
  injectDummyUserMessage: boolean = true,
15
- ): Promise<[Record<string, any>[], GoogleFormatData]> {
16
- const turns: Record<string, any>[] = [];
15
+ ): Promise<[Record<string, unknown>[], GoogleFormatData]> {
16
+ const turns: Record<string, unknown>[] = [];
17
17
  const systemMessages: string[] = [];
18
18
  let currentRole: string | null = null;
19
- let parts: Record<string, any>[] = [];
19
+ let parts: Record<string, unknown>[] = [];
20
20
 
21
21
  // Flatten all grouped tool calls to get individual messages
22
22
  const itemGroups = groupToolCalls(chatCtx);
@@ -104,7 +104,7 @@ export async function toChatCtx(
104
104
  ];
105
105
  }
106
106
 
107
- async function toImagePart(image: ImageContent): Promise<Record<string, any>> {
107
+ async function toImagePart(image: ImageContent): Promise<Record<string, unknown>> {
108
108
  const cacheKey = 'serialized_image';
109
109
  if (!image._cache[cacheKey]) {
110
110
  image._cache[cacheKey] = await serializeImage(image);
@@ -4,7 +4,12 @@
4
4
  import { VideoBufferType, VideoFrame } from '@livekit/rtc-node';
5
5
  import { beforeEach, describe, expect, it, vi } from 'vitest';
6
6
  import { initializeLogger } from '../../log.js';
7
- import { ChatContext, FunctionCall, FunctionCallOutput } from '../chat_context.js';
7
+ import {
8
+ AgentHandoffItem,
9
+ ChatContext,
10
+ FunctionCall,
11
+ FunctionCallOutput,
12
+ } from '../chat_context.js';
8
13
  import { serializeImage } from '../utils.js';
9
14
  import { toChatCtx } from './openai.js';
10
15
 
@@ -578,4 +583,53 @@ describe('toChatCtx', () => {
578
583
  },
579
584
  ]);
580
585
  });
586
+
587
+ it('should filter out agent handoff items', async () => {
588
+ const ctx = ChatContext.empty();
589
+
590
+ ctx.addMessage({ role: 'user', content: 'Hello' });
591
+
592
+ // Insert an agent handoff item
593
+ const handoff = new AgentHandoffItem({
594
+ oldAgentId: 'agent_1',
595
+ newAgentId: 'agent_2',
596
+ });
597
+ ctx.insert(handoff);
598
+
599
+ ctx.addMessage({ role: 'assistant', content: 'Hi there!' });
600
+
601
+ const result = await toChatCtx(ctx);
602
+
603
+ // Agent handoff should be filtered out, only messages should remain
604
+ expect(result).toEqual([
605
+ { role: 'user', content: 'Hello' },
606
+ { role: 'assistant', content: 'Hi there!' },
607
+ ]);
608
+ });
609
+
610
+ it('should handle multiple agent handoffs without errors', async () => {
611
+ const ctx = ChatContext.empty();
612
+
613
+ ctx.addMessage({ role: 'user', content: 'Start' });
614
+
615
+ // Multiple handoffs
616
+ ctx.insert(new AgentHandoffItem({ oldAgentId: undefined, newAgentId: 'agent_1' }));
617
+ ctx.addMessage({ role: 'assistant', content: 'Response from agent 1' });
618
+
619
+ ctx.insert(new AgentHandoffItem({ oldAgentId: 'agent_1', newAgentId: 'agent_2' }));
620
+ ctx.addMessage({ role: 'assistant', content: 'Response from agent 2' });
621
+
622
+ ctx.insert(new AgentHandoffItem({ oldAgentId: 'agent_2', newAgentId: 'agent_3' }));
623
+ ctx.addMessage({ role: 'assistant', content: 'Response from agent 3' });
624
+
625
+ const result = await toChatCtx(ctx);
626
+
627
+ // All handoffs should be filtered out
628
+ expect(result).toEqual([
629
+ { role: 'user', content: 'Start' },
630
+ { role: 'assistant', content: 'Response from agent 1' },
631
+ { role: 'assistant', content: 'Response from agent 2' },
632
+ { role: 'assistant', content: 'Response from agent 3' },
633
+ ]);
634
+ });
581
635
  });
@@ -78,9 +78,10 @@ async function toChatItem(item: ChatItem) {
78
78
  tool_call_id: item.callId,
79
79
  content: item.output,
80
80
  };
81
- } else {
82
- throw new Error(`Unsupported item type: ${item['type']}`);
83
81
  }
82
+ // Skip other item types (e.g., agent_handoff)
83
+ // These should be filtered by groupToolCalls, but this is a safety net
84
+ throw new Error(`Unsupported item type: ${item['type']}`);
84
85
  }
85
86
 
86
87
  async function toImageContent(content: ImageContent) {
@@ -19,6 +19,7 @@ export interface MessageGeneration {
19
19
  messageId: string;
20
20
  textStream: ReadableStream<string>;
21
21
  audioStream: ReadableStream<AudioFrame>;
22
+ modalities?: Promise<('text' | 'audio')[]>;
22
23
  }
23
24
 
24
25
  export interface GenerationCreatedEvent {
@@ -40,6 +41,7 @@ export interface RealtimeCapabilities {
40
41
  turnDetection: boolean;
41
42
  userTranscription: boolean;
42
43
  autoToolReplyGeneration: boolean;
44
+ audioOutput: boolean;
43
45
  }
44
46
 
45
47
  export interface InputTranscriptionCompleted {
@@ -121,7 +123,12 @@ export abstract class RealtimeSession extends EventEmitter {
121
123
  /**
122
124
  * Truncate the message at the given audio end time
123
125
  */
124
- abstract truncate(options: { messageId: string; audioEndMs: number }): Promise<void>;
126
+ abstract truncate(options: {
127
+ messageId: string;
128
+ audioEndMs: number;
129
+ modalities?: ('text' | 'audio')[];
130
+ audioTranscript?: string;
131
+ }): Promise<void>;
125
132
 
126
133
  async close(): Promise<void> {
127
134
  this._mainTask.cancel();
package/src/llm/utils.ts CHANGED
@@ -323,9 +323,14 @@ export function computeChatCtxDiff(oldCtx: ChatContext, newCtx: ChatContext): Di
323
323
  };
324
324
  }
325
325
 
326
- export function toJsonSchema(schema: ToolInputSchema<any>, isOpenai: boolean = true): JSONSchema7 {
326
+ export function toJsonSchema(
327
+ schema: ToolInputSchema<any>,
328
+ isOpenai: boolean = true,
329
+ strict: boolean = false,
330
+ ): JSONSchema7 {
327
331
  if (isZodSchema(schema)) {
328
- return zodSchemaToJsonSchema(schema, isOpenai);
332
+ return zodSchemaToJsonSchema(schema, isOpenai, strict);
329
333
  }
334
+
330
335
  return schema as JSONSchema7;
331
336
  }
@@ -260,6 +260,107 @@ describe('Zod Utils', () => {
260
260
  expect(jsonSchema7).toHaveProperty('properties');
261
261
  });
262
262
  });
263
+
264
+ describe('strict parameter', () => {
265
+ it('should produce strict JSON schema with strict: true', () => {
266
+ const schema = z4.object({
267
+ name: z4.string(),
268
+ age: z4.number(),
269
+ });
270
+
271
+ const strictSchema = zodSchemaToJsonSchema(schema, true, true);
272
+ expect(strictSchema).toMatchSnapshot();
273
+ });
274
+
275
+ it('should handle nullable fields in strict mode', () => {
276
+ const schema = z4.object({
277
+ required: z4.string(),
278
+ optional: z4.string().nullable(),
279
+ });
280
+
281
+ const strictSchema = zodSchemaToJsonSchema(schema, true, true);
282
+ expect(strictSchema).toMatchSnapshot();
283
+ });
284
+
285
+ it('should handle default values in strict mode', () => {
286
+ const schema = z4.object({
287
+ name: z4.string(),
288
+ role: z4.string().default('user'),
289
+ active: z4.boolean().default(true),
290
+ });
291
+
292
+ const strictSchema = zodSchemaToJsonSchema(schema, true, true);
293
+ expect(strictSchema).toMatchSnapshot();
294
+ });
295
+
296
+ it('should handle nested objects in strict mode', () => {
297
+ const schema = z4.object({
298
+ user: z4.object({
299
+ name: z4.string(),
300
+ email: z4.string().nullable(),
301
+ }),
302
+ metadata: z4.object({
303
+ created: z4.string(),
304
+ }),
305
+ });
306
+
307
+ const strictSchema = zodSchemaToJsonSchema(schema, true, true);
308
+ expect(strictSchema).toMatchSnapshot();
309
+ });
310
+
311
+ it('should handle arrays in strict mode', () => {
312
+ const schema = z4.object({
313
+ tags: z4.array(z4.string()),
314
+ numbers: z4.array(z4.number()),
315
+ });
316
+
317
+ const strictSchema = zodSchemaToJsonSchema(schema, true, true);
318
+ expect(strictSchema).toMatchSnapshot();
319
+ });
320
+
321
+ it('should handle v3 schemas in strict mode', () => {
322
+ const schema = z3.object({
323
+ name: z3.string(),
324
+ age: z3.number().optional(),
325
+ });
326
+
327
+ const strictSchema = zodSchemaToJsonSchema(schema, true, true);
328
+ expect(strictSchema).toMatchSnapshot();
329
+ });
330
+
331
+ it('should throw error when using .optional() without .nullable() in strict mode', () => {
332
+ const schema = z4.object({
333
+ required: z4.string(),
334
+ optional: z4.string().optional(),
335
+ });
336
+
337
+ expect(() => zodSchemaToJsonSchema(schema, true, true)).toThrow(
338
+ /uses `.optional\(\)` without `.nullable\(\)` which is not supported by the API/,
339
+ );
340
+ });
341
+
342
+ it('should throw error for nested .optional() fields in strict mode', () => {
343
+ const schema = z4.object({
344
+ user: z4.object({
345
+ name: z4.string(),
346
+ email: z4.string().optional(),
347
+ }),
348
+ });
349
+
350
+ expect(() => zodSchemaToJsonSchema(schema, true, true)).toThrow(
351
+ /uses `.optional\(\)` without `.nullable\(\)` which is not supported by the API/,
352
+ );
353
+ });
354
+
355
+ it('should NOT throw error when using .optional() in non-strict mode', () => {
356
+ const schema = z4.object({
357
+ required: z4.string(),
358
+ optional: z4.string().optional(),
359
+ });
360
+
361
+ expect(() => zodSchemaToJsonSchema(schema, true, false)).not.toThrow();
362
+ });
363
+ });
263
364
  });
264
365
 
265
366
  describe('parseZodSchema', () => {
@@ -2,6 +2,7 @@
2
2
  //
3
3
  // SPDX-License-Identifier: Apache-2.0
4
4
  import type { JSONSchema7 } from 'json-schema';
5
+ import { toStrictJsonSchema } from 'openai/lib/transform';
5
6
  import { zodToJsonSchema as zodToJsonSchemaV3 } from 'zod-to-json-schema';
6
7
  import type * as z3 from 'zod/v3';
7
8
  import * as z4 from 'zod/v4';
@@ -101,12 +102,18 @@ export function isZodObjectSchema(schema: ZodSchema): boolean {
101
102
  * @param isOpenai - Whether to use OpenAI-specific formatting (default: true)
102
103
  * @returns A JSON Schema representation of the Zod schema
103
104
  */
104
- export function zodSchemaToJsonSchema(schema: ZodSchema, isOpenai: boolean = true): JSONSchema7 {
105
+ export function zodSchemaToJsonSchema(
106
+ schema: ZodSchema,
107
+ isOpenai: boolean = true,
108
+ strict: boolean = false,
109
+ ): JSONSchema7 {
110
+ let result: JSONSchema7;
111
+
105
112
  if (isZod4Schema(schema)) {
106
113
  // Zod v4 has native toJSONSchema support
107
114
  // Configuration adapted from Vercel AI SDK to support OpenAPI conversion for Google
108
115
  // Source: https://github.com/vercel/ai/blob/main/packages/provider-utils/src/schema.ts#L255-L258
109
- return z4.toJSONSchema(schema, {
116
+ result = z4.toJSONSchema(schema, {
110
117
  target: 'draft-7',
111
118
  io: 'output',
112
119
  reused: 'inline', // Don't use references by default (to support openapi conversion for google)
@@ -115,11 +122,13 @@ export function zodSchemaToJsonSchema(schema: ZodSchema, isOpenai: boolean = tru
115
122
  // Zod v3 requires the zod-to-json-schema library
116
123
  // Configuration adapted from Vercel AI SDK
117
124
  // $refStrategy: 'none' is equivalent to v4's reused: 'inline'
118
- return zodToJsonSchemaV3(schema, {
125
+ result = zodToJsonSchemaV3(schema, {
119
126
  target: isOpenai ? 'openAi' : 'jsonSchema7',
120
127
  $refStrategy: 'none', // Don't use references by default (to support openapi conversion for google)
121
128
  }) as JSONSchema7;
122
129
  }
130
+
131
+ return strict ? (toStrictJsonSchema(result) as JSONSchema7) : result;
123
132
  }
124
133
 
125
134
  /**
package/src/log.ts CHANGED
@@ -42,4 +42,5 @@ export const initializeLogger = ({ pretty, level }: LoggerOptions) => {
42
42
  if (level) {
43
43
  logger.level = level;
44
44
  }
45
+ // TODO(brian): PR4 - Add Pino bridge to OTEL LoggingHandler for structured logging integration
45
46
  };
@@ -0,0 +1,10 @@
1
+ // SPDX-FileCopyrightText: 2025 LiveKit, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+
5
+ // TODO(brian): PR4 - Add logging integration exports
6
+ // TODO(brian): PR5 - Add uploadSessionReport export
7
+
8
+ export * as traceTypes from './trace_types.js';
9
+ export { setTracerProvider, setupCloudTracer, tracer, type StartSpanOptions } from './traces.js';
10
+ export { recordException, recordRealtimeMetrics } from './utils.js';
@@ -0,0 +1,88 @@
1
+ // SPDX-FileCopyrightText: 2025 LiveKit, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+
5
+ // LiveKit custom attributes
6
+ export const ATTR_SPEECH_ID = 'lk.speech_id';
7
+ export const ATTR_AGENT_LABEL = 'lk.agent_label';
8
+ export const ATTR_START_TIME = 'lk.start_time';
9
+ export const ATTR_END_TIME = 'lk.end_time';
10
+ export const ATTR_RETRY_COUNT = 'lk.retry_count';
11
+
12
+ export const ATTR_PARTICIPANT_ID = 'lk.participant_id';
13
+ export const ATTR_PARTICIPANT_IDENTITY = 'lk.participant_identity';
14
+ export const ATTR_PARTICIPANT_KIND = 'lk.participant_kind';
15
+
16
+ // session start
17
+ export const ATTR_JOB_ID = 'lk.job_id';
18
+ export const ATTR_AGENT_NAME = 'lk.agent_name';
19
+ export const ATTR_ROOM_NAME = 'lk.room_name';
20
+ export const ATTR_SESSION_OPTIONS = 'lk.session_options';
21
+
22
+ // assistant turn
23
+ export const ATTR_USER_INPUT = 'lk.user_input';
24
+ export const ATTR_INSTRUCTIONS = 'lk.instructions';
25
+ export const ATTR_SPEECH_INTERRUPTED = 'lk.interrupted';
26
+
27
+ // llm node
28
+ export const ATTR_CHAT_CTX = 'lk.chat_ctx';
29
+ export const ATTR_FUNCTION_TOOLS = 'lk.function_tools';
30
+ export const ATTR_RESPONSE_TEXT = 'lk.response.text';
31
+ export const ATTR_RESPONSE_FUNCTION_CALLS = 'lk.response.function_calls';
32
+
33
+ // function tool
34
+ export const ATTR_FUNCTION_TOOL_NAME = 'lk.function_tool.name';
35
+ export const ATTR_FUNCTION_TOOL_ARGS = 'lk.function_tool.arguments';
36
+ export const ATTR_FUNCTION_TOOL_IS_ERROR = 'lk.function_tool.is_error';
37
+ export const ATTR_FUNCTION_TOOL_OUTPUT = 'lk.function_tool.output';
38
+
39
+ // tts node
40
+ export const ATTR_TTS_INPUT_TEXT = 'lk.input_text';
41
+ export const ATTR_TTS_STREAMING = 'lk.tts.streaming';
42
+ export const ATTR_TTS_LABEL = 'lk.tts.label';
43
+
44
+ // eou detection
45
+ export const ATTR_EOU_PROBABILITY = 'lk.eou.probability';
46
+ export const ATTR_EOU_UNLIKELY_THRESHOLD = 'lk.eou.unlikely_threshold';
47
+ export const ATTR_EOU_DELAY = 'lk.eou.endpointing_delay';
48
+ export const ATTR_EOU_LANGUAGE = 'lk.eou.language';
49
+ export const ATTR_USER_TRANSCRIPT = 'lk.user_transcript';
50
+ export const ATTR_TRANSCRIPT_CONFIDENCE = 'lk.transcript_confidence';
51
+ export const ATTR_TRANSCRIPTION_DELAY = 'lk.transcription_delay';
52
+ export const ATTR_END_OF_TURN_DELAY = 'lk.end_of_turn_delay';
53
+
54
+ // metrics
55
+ export const ATTR_LLM_METRICS = 'lk.llm_metrics';
56
+ export const ATTR_TTS_METRICS = 'lk.tts_metrics';
57
+ export const ATTR_REALTIME_MODEL_METRICS = 'lk.realtime_model_metrics';
58
+
59
+ // OpenTelemetry GenAI attributes
60
+ // OpenTelemetry specification: https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/
61
+ export const ATTR_GEN_AI_OPERATION_NAME = 'gen_ai.operation.name';
62
+ export const ATTR_GEN_AI_REQUEST_MODEL = 'gen_ai.request.model';
63
+ export const ATTR_GEN_AI_USAGE_INPUT_TOKENS = 'gen_ai.usage.input_tokens';
64
+ export const ATTR_GEN_AI_USAGE_OUTPUT_TOKENS = 'gen_ai.usage.output_tokens';
65
+
66
+ // Unofficial OpenTelemetry GenAI attributes, recognized by LangFuse
67
+ // https://langfuse.com/integrations/native/opentelemetry#usage
68
+ // but not yet in the official OpenTelemetry specification.
69
+ export const ATTR_GEN_AI_USAGE_INPUT_TEXT_TOKENS = 'gen_ai.usage.input_text_tokens';
70
+ export const ATTR_GEN_AI_USAGE_INPUT_AUDIO_TOKENS = 'gen_ai.usage.input_audio_tokens';
71
+ export const ATTR_GEN_AI_USAGE_INPUT_CACHED_TOKENS = 'gen_ai.usage.input_cached_tokens';
72
+ export const ATTR_GEN_AI_USAGE_OUTPUT_TEXT_TOKENS = 'gen_ai.usage.output_text_tokens';
73
+ export const ATTR_GEN_AI_USAGE_OUTPUT_AUDIO_TOKENS = 'gen_ai.usage.output_audio_tokens';
74
+
75
+ // OpenTelemetry GenAI event names (for structured logging)
76
+ export const EVENT_GEN_AI_SYSTEM_MESSAGE = 'gen_ai.system.message';
77
+ export const EVENT_GEN_AI_USER_MESSAGE = 'gen_ai.user.message';
78
+ export const EVENT_GEN_AI_ASSISTANT_MESSAGE = 'gen_ai.assistant.message';
79
+ export const EVENT_GEN_AI_TOOL_MESSAGE = 'gen_ai.tool.message';
80
+ export const EVENT_GEN_AI_CHOICE = 'gen_ai.choice';
81
+
82
+ // Exception attributes
83
+ export const ATTR_EXCEPTION_TRACE = 'exception.stacktrace';
84
+ export const ATTR_EXCEPTION_TYPE = 'exception.type';
85
+ export const ATTR_EXCEPTION_MESSAGE = 'exception.message';
86
+
87
+ // Platform-specific attributes
88
+ export const ATTR_LANGFUSE_COMPLETION_START_TIME = 'langfuse.observation.completion_start_time';