@livekit/agents-plugin-openai 1.0.49 → 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.
Files changed (75) hide show
  1. package/dist/index.cjs +5 -2
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +1 -0
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +4 -2
  7. package/dist/index.js.map +1 -1
  8. package/dist/llm.test.cjs +31 -16
  9. package/dist/llm.test.cjs.map +1 -1
  10. package/dist/llm.test.js +32 -17
  11. package/dist/llm.test.js.map +1 -1
  12. package/dist/responses/llm.cjs +71 -16
  13. package/dist/responses/llm.cjs.map +1 -1
  14. package/dist/responses/llm.d.cts +10 -25
  15. package/dist/responses/llm.d.ts +10 -25
  16. package/dist/responses/llm.d.ts.map +1 -1
  17. package/dist/responses/llm.js +71 -14
  18. package/dist/responses/llm.js.map +1 -1
  19. package/dist/responses/llm.test.cjs +32 -17
  20. package/dist/responses/llm.test.cjs.map +1 -1
  21. package/dist/responses/llm.test.js +33 -18
  22. package/dist/responses/llm.test.js.map +1 -1
  23. package/dist/stt.cjs +7 -3
  24. package/dist/stt.cjs.map +1 -1
  25. package/dist/stt.d.ts.map +1 -1
  26. package/dist/stt.js +8 -4
  27. package/dist/stt.js.map +1 -1
  28. package/dist/stt.test.cjs +11 -3
  29. package/dist/stt.test.cjs.map +1 -1
  30. package/dist/stt.test.js +12 -4
  31. package/dist/stt.test.js.map +1 -1
  32. package/dist/tts.test.cjs +11 -3
  33. package/dist/tts.test.cjs.map +1 -1
  34. package/dist/tts.test.js +12 -4
  35. package/dist/tts.test.js.map +1 -1
  36. package/dist/ws/index.cjs +29 -0
  37. package/dist/ws/index.cjs.map +1 -0
  38. package/dist/ws/index.d.cts +3 -0
  39. package/dist/ws/index.d.ts +3 -0
  40. package/dist/ws/index.d.ts.map +1 -0
  41. package/dist/ws/index.js +5 -0
  42. package/dist/ws/index.js.map +1 -0
  43. package/dist/ws/llm.cjs +502 -0
  44. package/dist/ws/llm.cjs.map +1 -0
  45. package/dist/ws/llm.d.cts +74 -0
  46. package/dist/ws/llm.d.ts +74 -0
  47. package/dist/ws/llm.d.ts.map +1 -0
  48. package/dist/ws/llm.js +485 -0
  49. package/dist/ws/llm.js.map +1 -0
  50. package/dist/ws/llm.test.cjs +26 -0
  51. package/dist/ws/llm.test.cjs.map +1 -0
  52. package/dist/ws/llm.test.d.cts +2 -0
  53. package/dist/ws/llm.test.d.ts +2 -0
  54. package/dist/ws/llm.test.d.ts.map +1 -0
  55. package/dist/ws/llm.test.js +25 -0
  56. package/dist/ws/llm.test.js.map +1 -0
  57. package/dist/ws/types.cjs +128 -0
  58. package/dist/ws/types.cjs.map +1 -0
  59. package/dist/ws/types.d.cts +167 -0
  60. package/dist/ws/types.d.ts +167 -0
  61. package/dist/ws/types.d.ts.map +1 -0
  62. package/dist/ws/types.js +95 -0
  63. package/dist/ws/types.js.map +1 -0
  64. package/package.json +6 -5
  65. package/src/index.ts +1 -0
  66. package/src/llm.test.ts +31 -17
  67. package/src/responses/llm.test.ts +32 -18
  68. package/src/responses/llm.ts +105 -19
  69. package/src/stt.test.ts +12 -4
  70. package/src/stt.ts +8 -4
  71. package/src/tts.test.ts +12 -4
  72. package/src/ws/index.ts +17 -0
  73. package/src/ws/llm.test.ts +30 -0
  74. package/src/ws/llm.ts +665 -0
  75. package/src/ws/types.ts +131 -0
@@ -0,0 +1,95 @@
1
+ import { z } from "zod";
2
+ const wsResponseCreateEventSchema = z.object({
3
+ type: z.literal("response.create"),
4
+ model: z.string(),
5
+ input: z.array(z.unknown()),
6
+ tools: z.array(z.unknown()).optional(),
7
+ previous_response_id: z.string().nullable().optional(),
8
+ store: z.boolean().optional(),
9
+ temperature: z.number().optional(),
10
+ metadata: z.record(z.string(), z.string()).optional()
11
+ }).passthrough();
12
+ const wsResponseCreatedEventSchema = z.object({
13
+ type: z.literal("response.created"),
14
+ response: z.object({
15
+ id: z.string()
16
+ }).passthrough()
17
+ });
18
+ const wsFunctionCallItemSchema = z.object({
19
+ type: z.literal("function_call"),
20
+ call_id: z.string(),
21
+ name: z.string(),
22
+ arguments: z.string()
23
+ });
24
+ const wsOutputItemSchema = z.discriminatedUnion("type", [
25
+ wsFunctionCallItemSchema,
26
+ z.object({ type: z.literal("message") }).passthrough(),
27
+ z.object({ type: z.literal("reasoning") }).passthrough(),
28
+ z.object({ type: z.literal("file") }).passthrough(),
29
+ z.object({ type: z.literal("computer_call") }).passthrough(),
30
+ z.object({ type: z.literal("web_search_call") }).passthrough()
31
+ ]);
32
+ const wsOutputItemDoneEventSchema = z.object({
33
+ type: z.literal("response.output_item.done"),
34
+ item: wsOutputItemSchema
35
+ });
36
+ const wsOutputTextDeltaEventSchema = z.object({
37
+ type: z.literal("response.output_text.delta"),
38
+ delta: z.string()
39
+ });
40
+ const wsResponseCompletedEventSchema = z.object({
41
+ type: z.literal("response.completed"),
42
+ response: z.object({
43
+ id: z.string(),
44
+ usage: z.object({
45
+ output_tokens: z.number(),
46
+ input_tokens: z.number(),
47
+ total_tokens: z.number(),
48
+ input_tokens_details: z.object({
49
+ cached_tokens: z.number()
50
+ }).passthrough()
51
+ }).optional()
52
+ }).passthrough()
53
+ });
54
+ const wsResponseFailedEventSchema = z.object({
55
+ type: z.literal("response.failed"),
56
+ response: z.object({
57
+ id: z.string().optional(),
58
+ error: z.object({
59
+ code: z.string().optional(),
60
+ message: z.string().optional()
61
+ }).optional()
62
+ }).passthrough()
63
+ });
64
+ const wsErrorEventSchema = z.object({
65
+ type: z.literal("error"),
66
+ status: z.number().optional(),
67
+ error: z.object({
68
+ type: z.string().optional(),
69
+ code: z.string().optional(),
70
+ message: z.string().optional(),
71
+ param: z.string().optional()
72
+ }).optional(),
73
+ message: z.string().optional()
74
+ });
75
+ const wsServerEventSchema = z.discriminatedUnion("type", [
76
+ wsResponseCreatedEventSchema,
77
+ wsOutputItemDoneEventSchema,
78
+ wsOutputTextDeltaEventSchema,
79
+ wsResponseCompletedEventSchema,
80
+ wsResponseFailedEventSchema,
81
+ wsErrorEventSchema
82
+ ]);
83
+ export {
84
+ wsErrorEventSchema,
85
+ wsFunctionCallItemSchema,
86
+ wsOutputItemDoneEventSchema,
87
+ wsOutputItemSchema,
88
+ wsOutputTextDeltaEventSchema,
89
+ wsResponseCompletedEventSchema,
90
+ wsResponseCreateEventSchema,
91
+ wsResponseCreatedEventSchema,
92
+ wsResponseFailedEventSchema,
93
+ wsServerEventSchema
94
+ };
95
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/ws/types.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { z } from 'zod';\n\n// ============================================================================\n// Client → Server events\n// ============================================================================\n\nexport const wsResponseCreateEventSchema = z\n .object({\n type: z.literal('response.create'),\n model: z.string(),\n input: z.array(z.unknown()),\n tools: z.array(z.unknown()).optional(),\n previous_response_id: z.string().nullable().optional(),\n store: z.boolean().optional(),\n temperature: z.number().optional(),\n metadata: z.record(z.string(), z.string()).optional(),\n })\n .passthrough();\n\nexport type WsResponseCreateEvent = z.infer<typeof wsResponseCreateEventSchema>;\n\n// ============================================================================\n// Server → Client events\n// ============================================================================\n\nexport const wsResponseCreatedEventSchema = z.object({\n type: z.literal('response.created'),\n response: z\n .object({\n id: z.string(),\n })\n .passthrough(),\n});\n\nexport const wsFunctionCallItemSchema = z.object({\n type: z.literal('function_call'),\n call_id: z.string(),\n name: z.string(),\n arguments: z.string(),\n});\n\nexport const wsOutputItemSchema = z.discriminatedUnion('type', [\n wsFunctionCallItemSchema,\n z.object({ type: z.literal('message') }).passthrough(),\n z.object({ type: z.literal('reasoning') }).passthrough(),\n z.object({ type: z.literal('file') }).passthrough(),\n z.object({ type: z.literal('computer_call') }).passthrough(),\n z.object({ type: z.literal('web_search_call') }).passthrough(),\n]);\n\nexport const wsOutputItemDoneEventSchema = z.object({\n type: z.literal('response.output_item.done'),\n item: wsOutputItemSchema,\n});\n\nexport const wsOutputTextDeltaEventSchema = z.object({\n type: z.literal('response.output_text.delta'),\n delta: z.string(),\n});\n\nexport const wsResponseCompletedEventSchema = z.object({\n type: z.literal('response.completed'),\n response: z\n .object({\n id: z.string(),\n usage: z\n .object({\n output_tokens: z.number(),\n input_tokens: z.number(),\n total_tokens: z.number(),\n input_tokens_details: z\n .object({\n cached_tokens: z.number(),\n })\n .passthrough(),\n })\n .optional(),\n })\n .passthrough(),\n});\n\nexport const wsResponseFailedEventSchema = z.object({\n type: z.literal('response.failed'),\n response: z\n .object({\n id: z.string().optional(),\n error: z\n .object({\n code: z.string().optional(),\n message: z.string().optional(),\n })\n .optional(),\n })\n .passthrough(),\n});\n\nexport const wsErrorEventSchema = z.object({\n type: z.literal('error'),\n status: z.number().optional(),\n error: z\n .object({\n type: z.string().optional(),\n code: z.string().optional(),\n message: z.string().optional(),\n param: z.string().optional(),\n })\n .optional(),\n message: z.string().optional(),\n});\n\nexport const wsServerEventSchema = z.discriminatedUnion('type', [\n wsResponseCreatedEventSchema,\n wsOutputItemDoneEventSchema,\n wsOutputTextDeltaEventSchema,\n wsResponseCompletedEventSchema,\n wsResponseFailedEventSchema,\n wsErrorEventSchema,\n]);\n\nexport type WsResponseCreatedEvent = z.infer<typeof wsResponseCreatedEventSchema>;\nexport type WsFunctionCallItem = z.infer<typeof wsFunctionCallItemSchema>;\nexport type WsOutputItem = z.infer<typeof wsOutputItemSchema>;\nexport type WsOutputItemDoneEvent = z.infer<typeof wsOutputItemDoneEventSchema>;\nexport type WsOutputTextDeltaEvent = z.infer<typeof wsOutputTextDeltaEventSchema>;\nexport type WsResponseCompletedEvent = z.infer<typeof wsResponseCompletedEventSchema>;\nexport type WsResponseFailedEvent = z.infer<typeof wsResponseFailedEventSchema>;\nexport type WsErrorEvent = z.infer<typeof wsErrorEventSchema>;\nexport type WsServerEvent = z.infer<typeof wsServerEventSchema>;\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livekit/agents-plugin-openai",
3
- "version": "1.0.49",
3
+ "version": "1.0.51",
4
4
  "description": "OpenAI plugin for LiveKit Node Agents",
5
5
  "main": "dist/index.js",
6
6
  "require": "dist/index.cjs",
@@ -30,9 +30,9 @@
30
30
  "@types/ws": "^8.5.10",
31
31
  "tsup": "^8.3.5",
32
32
  "typescript": "^5.0.0",
33
- "@livekit/agents": "1.0.49",
34
- "@livekit/agents-plugin-silero": "1.0.49",
35
- "@livekit/agents-plugins-test": "1.0.49"
33
+ "@livekit/agents": "1.0.51",
34
+ "@livekit/agents-plugin-silero": "1.0.51",
35
+ "@livekit/agents-plugins-test": "1.0.51"
36
36
  },
37
37
  "dependencies": {
38
38
  "@livekit/mutex": "^1.1.1",
@@ -41,7 +41,8 @@
41
41
  },
42
42
  "peerDependencies": {
43
43
  "@livekit/rtc-node": "^0.13.24",
44
- "@livekit/agents": "1.0.49"
44
+ "zod": "^3.25.76 || ^4.1.8",
45
+ "@livekit/agents": "1.0.51"
45
46
  },
46
47
  "scripts": {
47
48
  "build": "tsup --onSuccess \"pnpm build:types\"",
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@ export * as realtime from './realtime/index.js';
9
9
  export * as responses from './responses/index.js';
10
10
  export { STT, type STTOptions } from './stt.js';
11
11
  export { ChunkedStream, TTS, type TTSOptions } from './tts.js';
12
+ export * as ws from './ws/index.js';
12
13
 
13
14
  class OpenAIPlugin extends Plugin {
14
15
  constructor() {
package/src/llm.test.ts CHANGED
@@ -2,23 +2,37 @@
2
2
  //
3
3
  // SPDX-License-Identifier: Apache-2.0
4
4
  import { llm, llmStrict } from '@livekit/agents-plugins-test';
5
- import { describe } from 'vitest';
5
+ import { describe, it } from 'vitest';
6
6
  import { LLM } from './llm.js';
7
7
 
8
- describe('OpenAI', async () => {
9
- await llm(
10
- new LLM({
11
- temperature: 0,
12
- }),
13
- false,
14
- );
15
- });
8
+ const hasOpenAIApiKey = Boolean(process.env.OPENAI_API_KEY);
16
9
 
17
- describe('OpenAI strict tool schema', async () => {
18
- await llmStrict(
19
- new LLM({
20
- temperature: 0,
21
- strictToolSchema: true,
22
- }),
23
- );
24
- });
10
+ if (hasOpenAIApiKey) {
11
+ describe('OpenAI', async () => {
12
+ await llm(
13
+ new LLM({
14
+ temperature: 0,
15
+ }),
16
+ false,
17
+ );
18
+ });
19
+ } else {
20
+ describe('OpenAI', () => {
21
+ it.skip('requires OPENAI_API_KEY', () => {});
22
+ });
23
+ }
24
+
25
+ if (hasOpenAIApiKey) {
26
+ describe('OpenAI strict tool schema', async () => {
27
+ await llmStrict(
28
+ new LLM({
29
+ temperature: 0,
30
+ strictToolSchema: true,
31
+ }),
32
+ );
33
+ });
34
+ } else {
35
+ describe('OpenAI strict tool schema', () => {
36
+ it.skip('requires OPENAI_API_KEY', () => {});
37
+ });
38
+ }
@@ -2,24 +2,38 @@
2
2
  //
3
3
  // SPDX-License-Identifier: Apache-2.0
4
4
  import { llm, llmStrict } from '@livekit/agents-plugins-test';
5
- import { describe } from 'vitest';
5
+ import { describe, it } from 'vitest';
6
6
  import { LLM } from './llm.js';
7
7
 
8
- describe('OpenAI Responses', async () => {
9
- await llm(
10
- new LLM({
11
- temperature: 0,
12
- strictToolSchema: false,
13
- }),
14
- true,
15
- );
16
- });
8
+ const hasOpenAIApiKey = Boolean(process.env.OPENAI_API_KEY);
17
9
 
18
- describe('OpenAI Responses strict tool schema', async () => {
19
- await llmStrict(
20
- new LLM({
21
- temperature: 0,
22
- strictToolSchema: true,
23
- }),
24
- );
25
- });
10
+ if (hasOpenAIApiKey) {
11
+ describe('OpenAI Responses', async () => {
12
+ await llm(
13
+ new LLM({
14
+ temperature: 0,
15
+ strictToolSchema: false,
16
+ }),
17
+ true,
18
+ );
19
+ });
20
+ } else {
21
+ describe('OpenAI Responses', () => {
22
+ it.skip('requires OPENAI_API_KEY', () => {});
23
+ });
24
+ }
25
+
26
+ if (hasOpenAIApiKey) {
27
+ describe('OpenAI Responses strict tool schema', async () => {
28
+ await llmStrict(
29
+ new LLM({
30
+ temperature: 0,
31
+ strictToolSchema: true,
32
+ }),
33
+ );
34
+ });
35
+ } else {
36
+ describe('OpenAI Responses strict tool schema', () => {
37
+ it.skip('requires OPENAI_API_KEY', () => {});
38
+ });
39
+ }
@@ -8,12 +8,14 @@ import {
8
8
  APITimeoutError,
9
9
  DEFAULT_API_CONNECT_OPTIONS,
10
10
  llm,
11
+ log,
11
12
  toError,
12
13
  } from '@livekit/agents';
13
14
  import OpenAI from 'openai';
14
15
  import type { ChatModels } from '../models.js';
16
+ import { WSLLM } from '../ws/llm.js';
15
17
 
16
- interface LLMOptions {
18
+ export interface LLMOptions {
17
19
  model: string | ChatModels;
18
20
  apiKey?: string;
19
21
  baseURL?: string;
@@ -24,30 +26,32 @@ interface LLMOptions {
24
26
  store?: boolean;
25
27
  metadata?: Record<string, string>;
26
28
  strictToolSchema?: boolean;
29
+
30
+ /**
31
+ * Whether to use the WebSocket API.
32
+ * @default true
33
+ */
34
+ useWebSocket?: boolean;
27
35
  }
28
36
 
37
+ type HttpLLMOptions = Omit<LLMOptions, 'useWebSocket'>;
38
+
29
39
  const defaultLLMOptions: LLMOptions = {
30
40
  model: 'gpt-4.1',
31
41
  apiKey: process.env.OPENAI_API_KEY,
32
42
  strictToolSchema: true,
43
+ useWebSocket: true,
33
44
  };
34
45
 
35
- export class LLM extends llm.LLM {
46
+ class ResponsesHttpLLM extends llm.LLM {
36
47
  #client: OpenAI;
37
- #opts: LLMOptions;
48
+ #opts: HttpLLMOptions;
38
49
 
39
- /**
40
- * Create a new instance of OpenAI Responses LLM.
41
- *
42
- * @remarks
43
- * `apiKey` must be set to your OpenAI API key, either using the argument or by setting the
44
- * `OPENAI_API_KEY` environment variable.
45
- */
46
- constructor(opts: Partial<LLMOptions> = defaultLLMOptions) {
50
+ constructor(opts: Partial<HttpLLMOptions> = defaultLLMOptions) {
47
51
  super();
48
52
 
49
53
  this.#opts = { ...defaultLLMOptions, ...opts };
50
- if (this.#opts.apiKey === undefined) {
54
+ if (this.#opts.apiKey === undefined && this.#opts.client === undefined) {
51
55
  throw new Error('OpenAI API key is required, whether as an argument or as $OPENAI_API_KEY');
52
56
  }
53
57
 
@@ -59,15 +63,15 @@ export class LLM extends llm.LLM {
59
63
  });
60
64
  }
61
65
 
62
- label(): string {
66
+ override label(): string {
63
67
  return 'openai.responses.LLM';
64
68
  }
65
69
 
66
- get model(): string {
70
+ override get model(): string {
67
71
  return this.#opts.model;
68
72
  }
69
73
 
70
- chat({
74
+ override chat({
71
75
  chatCtx,
72
76
  toolCtx,
73
77
  connOptions = DEFAULT_API_CONNECT_OPTIONS,
@@ -81,7 +85,7 @@ export class LLM extends llm.LLM {
81
85
  parallelToolCalls?: boolean;
82
86
  toolChoice?: llm.ToolChoice;
83
87
  extraKwargs?: Record<string, unknown>;
84
- }): LLMStream {
88
+ }): ResponsesHttpLLMStream {
85
89
  const modelOptions: Record<string, unknown> = { ...(extraKwargs || {}) };
86
90
 
87
91
  parallelToolCalls =
@@ -110,7 +114,7 @@ export class LLM extends llm.LLM {
110
114
  modelOptions.metadata = this.#opts.metadata;
111
115
  }
112
116
 
113
- return new LLMStream(this, {
117
+ return new ResponsesHttpLLMStream(this, {
114
118
  model: this.#opts.model,
115
119
  client: this.#client,
116
120
  chatCtx,
@@ -122,7 +126,7 @@ export class LLM extends llm.LLM {
122
126
  }
123
127
  }
124
128
 
125
- export class LLMStream extends llm.LLMStream {
129
+ class ResponsesHttpLLMStream extends llm.LLMStream {
126
130
  private model: string | ChatModels;
127
131
  private client: OpenAI;
128
132
  private modelOptions: Record<string, unknown>;
@@ -130,7 +134,7 @@ export class LLMStream extends llm.LLMStream {
130
134
  private responseId: string;
131
135
 
132
136
  constructor(
133
- llm: LLM,
137
+ llm: ResponsesHttpLLM,
134
138
  {
135
139
  model,
136
140
  client,
@@ -325,3 +329,85 @@ export class LLMStream extends llm.LLMStream {
325
329
  return undefined;
326
330
  }
327
331
  }
332
+
333
+ export class LLM extends llm.LLM {
334
+ #opts: LLMOptions;
335
+ #llm: llm.LLM;
336
+ #logger = log();
337
+
338
+ /**
339
+ * Create a new instance of OpenAI Responses LLM.
340
+ *
341
+ * @remarks
342
+ * `apiKey` must be set to your OpenAI API key, either using the argument or by setting the
343
+ * `OPENAI_API_KEY` environment variable.
344
+ */
345
+ constructor(opts: Partial<LLMOptions> = defaultLLMOptions) {
346
+ super();
347
+
348
+ this.#opts = { ...defaultLLMOptions, ...opts };
349
+ const { useWebSocket, client, ...baseOpts } = this.#opts;
350
+
351
+ if (useWebSocket) {
352
+ if (client !== undefined) {
353
+ this.#logger.warn(
354
+ 'WebSocket mode does not support custom client; provided client will be ignored',
355
+ );
356
+ }
357
+ this.#llm = new WSLLM(baseOpts);
358
+ } else {
359
+ this.#llm = new ResponsesHttpLLM({ ...baseOpts, client });
360
+ }
361
+
362
+ // Forward events from the inner delegate so consumers listening on this
363
+ // wrapper instance (e.g. AgentActivity) receive them.
364
+ this.#llm.on('metrics_collected', (metrics) => this.emit('metrics_collected', metrics));
365
+ this.#llm.on('error', (error) => this.emit('error', error));
366
+ }
367
+
368
+ override label(): string {
369
+ return this.#llm.label();
370
+ }
371
+
372
+ override get model(): string {
373
+ return this.#llm.model;
374
+ }
375
+
376
+ override prewarm(): void {
377
+ this.#llm.prewarm();
378
+ }
379
+
380
+ // Ref: python livekit-plugins/livekit-plugins-openai/livekit/plugins/openai/responses/llm.py - 229-233 lines
381
+ override async aclose(): Promise<void> {
382
+ await this.#llm.aclose();
383
+ }
384
+
385
+ async close(): Promise<void> {
386
+ await this.aclose();
387
+ }
388
+
389
+ override chat({
390
+ chatCtx,
391
+ toolCtx,
392
+ connOptions = DEFAULT_API_CONNECT_OPTIONS,
393
+ parallelToolCalls,
394
+ toolChoice,
395
+ extraKwargs,
396
+ }: {
397
+ chatCtx: llm.ChatContext;
398
+ toolCtx?: llm.ToolContext;
399
+ connOptions?: APIConnectOptions;
400
+ parallelToolCalls?: boolean;
401
+ toolChoice?: llm.ToolChoice;
402
+ extraKwargs?: Record<string, unknown>;
403
+ }): llm.LLMStream {
404
+ return this.#llm.chat({
405
+ chatCtx,
406
+ toolCtx,
407
+ connOptions,
408
+ parallelToolCalls,
409
+ toolChoice,
410
+ extraKwargs,
411
+ });
412
+ }
413
+ }
package/src/stt.test.ts CHANGED
@@ -3,9 +3,17 @@
3
3
  // SPDX-License-Identifier: Apache-2.0
4
4
  import { VAD } from '@livekit/agents-plugin-silero';
5
5
  import { stt } from '@livekit/agents-plugins-test';
6
- import { describe } from 'vitest';
6
+ import { describe, it } from 'vitest';
7
7
  import { STT } from './stt.js';
8
8
 
9
- describe('OpenAI', async () => {
10
- await stt(new STT(), await VAD.load(), { streaming: false });
11
- });
9
+ const hasOpenAIApiKey = Boolean(process.env.OPENAI_API_KEY);
10
+
11
+ if (hasOpenAIApiKey) {
12
+ describe('OpenAI', async () => {
13
+ await stt(new STT(), await VAD.load(), { streaming: false });
14
+ });
15
+ } else {
16
+ describe('OpenAI', () => {
17
+ it.skip('requires OPENAI_API_KEY', () => {});
18
+ });
19
+ }
package/src/stt.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // SPDX-FileCopyrightText: 2024 LiveKit, Inc.
2
2
  //
3
3
  // SPDX-License-Identifier: Apache-2.0
4
- import { type AudioBuffer, mergeFrames, stt } from '@livekit/agents';
4
+ import { type AudioBuffer, mergeFrames, normalizeLanguage, stt } from '@livekit/agents';
5
5
  import type { AudioFrame } from '@livekit/rtc-node';
6
6
  import { OpenAI } from 'openai';
7
7
  import type { GroqAudioModels, WhisperModels } from './models.js';
@@ -38,7 +38,11 @@ export class STT extends stt.STT {
38
38
  constructor(opts: Partial<STTOptions> = defaultSTTOptions) {
39
39
  super({ streaming: false, interimResults: false, alignedTranscript: false });
40
40
 
41
- this.#opts = { ...defaultSTTOptions, ...opts };
41
+ this.#opts = {
42
+ ...defaultSTTOptions,
43
+ ...opts,
44
+ language: normalizeLanguage(opts.language ?? defaultSTTOptions.language),
45
+ };
42
46
  if (this.#opts.apiKey === undefined) {
43
47
  throw new Error('OpenAI API key is required, whether as an argument or as $OPENAI_API_KEY');
44
48
  }
@@ -113,7 +117,7 @@ export class STT extends stt.STT {
113
117
 
114
118
  #sanitizeOptions(language?: string): STTOptions {
115
119
  if (language) {
116
- return { ...this.#opts, language };
120
+ return { ...this.#opts, language: normalizeLanguage(language) };
117
121
  } else {
118
122
  return this.#opts;
119
123
  }
@@ -165,7 +169,7 @@ export class STT extends stt.STT {
165
169
  alternatives: [
166
170
  {
167
171
  text: resp.text || '',
168
- language: config.language || '',
172
+ language: normalizeLanguage(config.language || ''),
169
173
  startTime: 0,
170
174
  endTime: 0,
171
175
  confidence: 0,
package/src/tts.test.ts CHANGED
@@ -2,10 +2,18 @@
2
2
  //
3
3
  // SPDX-License-Identifier: Apache-2.0
4
4
  import { tts } from '@livekit/agents-plugins-test';
5
- import { describe } from 'vitest';
5
+ import { describe, it } from 'vitest';
6
6
  import { STT } from './stt.js';
7
7
  import { TTS } from './tts.js';
8
8
 
9
- describe('OpenAI', async () => {
10
- await tts(new TTS(), new STT(), { streaming: false });
11
- });
9
+ const hasOpenAIApiKey = Boolean(process.env.OPENAI_API_KEY);
10
+
11
+ if (hasOpenAIApiKey) {
12
+ describe('OpenAI', async () => {
13
+ await tts(new TTS(), new STT(), { streaming: false });
14
+ });
15
+ } else {
16
+ describe('OpenAI', () => {
17
+ it.skip('requires OPENAI_API_KEY', () => {});
18
+ });
19
+ }
@@ -0,0 +1,17 @@
1
+ // SPDX-FileCopyrightText: 2025 LiveKit, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+
5
+ export { ResponsesWebSocket } from './llm.js';
6
+ export type {
7
+ WsErrorEvent,
8
+ WsFunctionCallItem,
9
+ WsOutputItem,
10
+ WsOutputItemDoneEvent,
11
+ WsOutputTextDeltaEvent,
12
+ WsResponseCompletedEvent,
13
+ WsResponseCreateEvent,
14
+ WsResponseCreatedEvent,
15
+ WsResponseFailedEvent,
16
+ WsServerEvent,
17
+ } from './types.js';
@@ -0,0 +1,30 @@
1
+ // SPDX-FileCopyrightText: 2025 LiveKit, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ import { initializeLogger } from '@livekit/agents';
5
+ import { llm, llmStrict } from '@livekit/agents-plugins-test';
6
+ import { describe } from 'vitest';
7
+ import { LLM } from '../responses/llm.js';
8
+
9
+ initializeLogger({ level: 'silent', pretty: false });
10
+
11
+ describe('OpenAI Responses WS wrapper', async () => {
12
+ await llm(
13
+ new LLM({
14
+ temperature: 0,
15
+ strictToolSchema: false,
16
+ useWebSocket: true,
17
+ }),
18
+ true,
19
+ );
20
+ });
21
+
22
+ describe('OpenAI Responses WS wrapper strict tool schema', async () => {
23
+ await llmStrict(
24
+ new LLM({
25
+ temperature: 0,
26
+ strictToolSchema: true,
27
+ useWebSocket: true,
28
+ }),
29
+ );
30
+ });