@livekit/agents-plugin-baseten 0.0.0-next-20260624041820

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 (67) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +92 -0
  3. package/dist/index.cjs +48 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +5 -0
  6. package/dist/index.d.ts +5 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +21 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/llm.cjs +156 -0
  11. package/dist/llm.cjs.map +1 -0
  12. package/dist/llm.d.cts +47 -0
  13. package/dist/llm.d.ts +47 -0
  14. package/dist/llm.d.ts.map +1 -0
  15. package/dist/llm.js +130 -0
  16. package/dist/llm.js.map +1 -0
  17. package/dist/llm.test.cjs +22 -0
  18. package/dist/llm.test.cjs.map +1 -0
  19. package/dist/llm.test.d.cts +2 -0
  20. package/dist/llm.test.d.ts +2 -0
  21. package/dist/llm.test.d.ts.map +1 -0
  22. package/dist/llm.test.js +21 -0
  23. package/dist/llm.test.js.map +1 -0
  24. package/dist/stt.cjs +287 -0
  25. package/dist/stt.cjs.map +1 -0
  26. package/dist/stt.d.cts +18 -0
  27. package/dist/stt.d.ts +18 -0
  28. package/dist/stt.d.ts.map +1 -0
  29. package/dist/stt.js +269 -0
  30. package/dist/stt.js.map +1 -0
  31. package/dist/stt.test.cjs +19 -0
  32. package/dist/stt.test.cjs.map +1 -0
  33. package/dist/stt.test.d.cts +2 -0
  34. package/dist/stt.test.d.ts +2 -0
  35. package/dist/stt.test.d.ts.map +1 -0
  36. package/dist/stt.test.js +18 -0
  37. package/dist/stt.test.js.map +1 -0
  38. package/dist/tts.cjs +162 -0
  39. package/dist/tts.cjs.map +1 -0
  40. package/dist/tts.d.cts +45 -0
  41. package/dist/tts.d.ts +45 -0
  42. package/dist/tts.d.ts.map +1 -0
  43. package/dist/tts.js +142 -0
  44. package/dist/tts.js.map +1 -0
  45. package/dist/tts.test.cjs +19 -0
  46. package/dist/tts.test.cjs.map +1 -0
  47. package/dist/tts.test.d.cts +2 -0
  48. package/dist/tts.test.d.ts +2 -0
  49. package/dist/tts.test.d.ts.map +1 -0
  50. package/dist/tts.test.js +18 -0
  51. package/dist/tts.test.js.map +1 -0
  52. package/dist/types.cjs +17 -0
  53. package/dist/types.cjs.map +1 -0
  54. package/dist/types.d.cts +69 -0
  55. package/dist/types.d.ts +69 -0
  56. package/dist/types.d.ts.map +1 -0
  57. package/dist/types.js +1 -0
  58. package/dist/types.js.map +1 -0
  59. package/package.json +68 -0
  60. package/src/index.ts +20 -0
  61. package/src/llm.test.ts +24 -0
  62. package/src/llm.ts +195 -0
  63. package/src/stt.test.ts +22 -0
  64. package/src/stt.ts +335 -0
  65. package/src/tts.test.ts +21 -0
  66. package/src/tts.ts +205 -0
  67. package/src/types.ts +70 -0
package/src/llm.ts ADDED
@@ -0,0 +1,195 @@
1
+ // SPDX-FileCopyrightText: 2024 LiveKit, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+
5
+ /**
6
+ * Baseten LLM plugin for LiveKit Agents
7
+ * Configures the OpenAI plugin to work with Baseten's OpenAI-compatible API
8
+ */
9
+ import type { APIConnectOptions } from '@livekit/agents';
10
+ import { DEFAULT_API_CONNECT_OPTIONS, inference, llm } from '@livekit/agents';
11
+ import { OpenAI } from 'openai';
12
+ import type { BasetenLLMOptions } from './types.js';
13
+
14
+ export interface LLMOptions {
15
+ model: string;
16
+ apiKey?: string;
17
+ baseURL?: string;
18
+ user?: string;
19
+ temperature?: number;
20
+ topP?: number;
21
+ presencePenalty?: number;
22
+ frequencyPenalty?: number;
23
+ client?: OpenAI;
24
+ toolChoice?: llm.ToolChoice;
25
+ parallelToolCalls?: boolean;
26
+ metadata?: Record<string, string>;
27
+ maxCompletionTokens?: number;
28
+ serviceTier?: string;
29
+ store?: boolean;
30
+ strictToolSchema?: boolean;
31
+ }
32
+
33
+ const defaultLLMOptions: LLMOptions = {
34
+ model: 'openai/gpt-4o-mini',
35
+ apiKey: process.env.OPENAI_API_KEY,
36
+ parallelToolCalls: true,
37
+ strictToolSchema: false,
38
+ };
39
+
40
+ export class OpenAILLM extends llm.LLM {
41
+ #opts: LLMOptions;
42
+ #client: OpenAI;
43
+ #providerFmt: llm.ProviderFormat;
44
+
45
+ constructor(
46
+ opts: Partial<LLMOptions> = defaultLLMOptions,
47
+ providerFmt: llm.ProviderFormat = 'openai',
48
+ ) {
49
+ super();
50
+
51
+ this.#opts = { ...defaultLLMOptions, ...opts };
52
+ this.#providerFmt = providerFmt;
53
+ if (this.#opts.apiKey === undefined) {
54
+ throw new Error('OpenAI API key is required, whether as an argument or as $OPENAI_API_KEY');
55
+ }
56
+
57
+ this.#client =
58
+ this.#opts.client ||
59
+ new OpenAI({
60
+ baseURL: opts.baseURL,
61
+ apiKey: opts.apiKey,
62
+ });
63
+ }
64
+
65
+ label(): string {
66
+ return 'openai.LLM';
67
+ }
68
+
69
+ get model(): string {
70
+ return this.#opts.model;
71
+ }
72
+
73
+ chat({
74
+ chatCtx,
75
+ toolCtx: toolCtxInput,
76
+ connOptions = DEFAULT_API_CONNECT_OPTIONS,
77
+ parallelToolCalls,
78
+ toolChoice,
79
+ extraKwargs,
80
+ }: {
81
+ chatCtx: llm.ChatContext;
82
+ toolCtx?: llm.ToolContextLike;
83
+ connOptions?: APIConnectOptions;
84
+ parallelToolCalls?: boolean;
85
+ toolChoice?: llm.ToolChoice;
86
+ extraKwargs?: Record<string, unknown>;
87
+ }): inference.LLMStream {
88
+ const toolCtx = llm.toToolContext(toolCtxInput);
89
+ const extras: Record<string, unknown> = { ...extraKwargs };
90
+
91
+ if (this.#opts.metadata) {
92
+ extras.metadata = this.#opts.metadata;
93
+ }
94
+
95
+ if (this.#opts.user) {
96
+ extras.user = this.#opts.user;
97
+ }
98
+
99
+ if (this.#opts.maxCompletionTokens) {
100
+ extras.max_completion_tokens = this.#opts.maxCompletionTokens;
101
+ }
102
+
103
+ if (this.#opts.temperature !== undefined) {
104
+ extras.temperature = this.#opts.temperature;
105
+ }
106
+
107
+ if (this.#opts.topP !== undefined) {
108
+ extras.top_p = this.#opts.topP;
109
+ }
110
+
111
+ if (this.#opts.presencePenalty !== undefined) {
112
+ extras.presence_penalty = this.#opts.presencePenalty;
113
+ }
114
+
115
+ if (this.#opts.frequencyPenalty !== undefined) {
116
+ extras.frequency_penalty = this.#opts.frequencyPenalty;
117
+ }
118
+
119
+ if (this.#opts.serviceTier) {
120
+ extras.service_tier = this.#opts.serviceTier;
121
+ }
122
+
123
+ if (this.#opts.store !== undefined) {
124
+ extras.store = this.#opts.store;
125
+ }
126
+
127
+ parallelToolCalls =
128
+ parallelToolCalls !== undefined ? parallelToolCalls : this.#opts.parallelToolCalls;
129
+ if (
130
+ toolCtx &&
131
+ Object.keys(toolCtx.functionTools).length > 0 &&
132
+ parallelToolCalls !== undefined
133
+ ) {
134
+ extras.parallel_tool_calls = parallelToolCalls;
135
+ }
136
+
137
+ toolChoice = toolChoice !== undefined ? toolChoice : this.#opts.toolChoice;
138
+ if (toolChoice) {
139
+ extras.tool_choice = toolChoice;
140
+ }
141
+
142
+ return new LLMStream(this as unknown as inference.LLM, {
143
+ model: this.#opts.model,
144
+ providerFmt: this.#providerFmt,
145
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
146
+ client: this.#client as any,
147
+ chatCtx,
148
+ toolCtx,
149
+ connOptions,
150
+ modelOptions: extras,
151
+ strictToolSchema: this.#opts.strictToolSchema || false,
152
+ gatewayOptions: undefined, // OpenAI plugin doesn't use gateway authentication
153
+ });
154
+ }
155
+ }
156
+
157
+ export class LLMStream extends inference.LLMStream {}
158
+
159
+ export class LLM extends OpenAILLM {
160
+ constructor(opts: BasetenLLMOptions) {
161
+ const apiKey = opts.apiKey ?? process.env.BASETEN_API_KEY;
162
+ if (!apiKey) {
163
+ throw new Error(
164
+ 'Baseten API key is required. Set BASETEN_API_KEY environment variable or pass apiKey in options.',
165
+ );
166
+ }
167
+
168
+ if (!opts.model) {
169
+ throw new Error(
170
+ 'Model is required. Please specify a model name (e.g., "openai/gpt-4o-mini").',
171
+ );
172
+ }
173
+
174
+ const model = opts.model;
175
+
176
+ // Configure the OpenAI plugin with Baseten's endpoint
177
+ super({
178
+ model,
179
+ apiKey,
180
+ baseURL: 'https://inference.baseten.co/v1',
181
+ temperature: opts.temperature,
182
+ topP: opts.topP,
183
+ presencePenalty: opts.presencePenalty,
184
+ frequencyPenalty: opts.frequencyPenalty,
185
+ user: opts.user,
186
+ maxCompletionTokens: opts.maxTokens,
187
+ toolChoice: opts.toolChoice,
188
+ parallelToolCalls: opts.parallelToolCalls,
189
+ });
190
+ }
191
+
192
+ label(): string {
193
+ return 'baseten.LLM';
194
+ }
195
+ }
@@ -0,0 +1,22 @@
1
+ // SPDX-FileCopyrightText: 2024 LiveKit, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ import { VAD } from '@livekit/agents-plugin-silero';
5
+ import { stt } from '@livekit/agents-plugins-test';
6
+ import { describe, it } from 'vitest';
7
+ import { STT } from './stt.js';
8
+
9
+ const hasBasetenStreamingConfig = Boolean(
10
+ process.env.BASETEN_API_KEY &&
11
+ (process.env.BASETEN_MODEL_ENDPOINT || process.env.BASETEN_STT_MODEL_ID),
12
+ );
13
+
14
+ if (hasBasetenStreamingConfig) {
15
+ describe('Baseten', async () => {
16
+ await stt(new STT(), await VAD.load(), { streaming: true });
17
+ });
18
+ } else {
19
+ describe('Baseten', () => {
20
+ it.skip('requires Baseten streaming credentials/config', () => {});
21
+ });
22
+ }
package/src/stt.ts ADDED
@@ -0,0 +1,335 @@
1
+ // SPDX-FileCopyrightText: 2024 LiveKit, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ import {
5
+ type AudioBuffer,
6
+ AudioByteStream,
7
+ Task,
8
+ log,
9
+ normalizeLanguage,
10
+ stt,
11
+ waitForAbort,
12
+ } from '@livekit/agents';
13
+ import type { AudioFrame } from '@livekit/rtc-node';
14
+ import { WebSocket } from 'ws';
15
+ import type { BasetenSttOptions } from './types.js';
16
+
17
+ const defaultSTTOptions: Partial<BasetenSttOptions> = {
18
+ environment: 'production',
19
+ encoding: 'pcm_s16le',
20
+ sampleRate: 16000,
21
+ bufferSizeSeconds: 0.032,
22
+ enablePartialTranscripts: true,
23
+ partialTranscriptIntervalS: 0.5,
24
+ finalTranscriptMaxDurationS: 5,
25
+ audioLanguage: 'en',
26
+ languageDetectionOnly: false,
27
+ vadThreshold: 0.5,
28
+ vadMinSilenceDurationMs: 300,
29
+ vadSpeechPadMs: 30,
30
+ };
31
+
32
+ export class STT extends stt.STT {
33
+ #opts: BasetenSttOptions;
34
+ #logger = log();
35
+ label = 'baseten.STT';
36
+
37
+ constructor(opts: Partial<BasetenSttOptions> = {}) {
38
+ super({
39
+ streaming: true,
40
+ interimResults: opts.enablePartialTranscripts ?? defaultSTTOptions.enablePartialTranscripts!,
41
+ alignedTranscript: 'word',
42
+ });
43
+
44
+ const apiKey = opts.apiKey ?? process.env.BASETEN_API_KEY;
45
+ const modelEndpoint = opts.modelEndpoint ?? process.env.BASETEN_MODEL_ENDPOINT;
46
+ const modelId = opts.modelId ?? process.env.BASETEN_STT_MODEL_ID;
47
+
48
+ if (!apiKey) {
49
+ throw new Error(
50
+ 'Baseten API key is required, either pass it as `apiKey` or set $BASETEN_API_KEY',
51
+ );
52
+ }
53
+ if (!modelEndpoint && !modelId) {
54
+ throw new Error(
55
+ 'Baseten model endpoint is required, either pass it as `modelEndpoint` or set $BASETEN_MODEL_ENDPOINT',
56
+ );
57
+ }
58
+
59
+ this.#opts = {
60
+ ...defaultSTTOptions,
61
+ ...opts,
62
+ apiKey,
63
+ modelEndpoint,
64
+ modelId,
65
+ audioLanguage: normalizeLanguage((opts.audioLanguage ?? defaultSTTOptions.audioLanguage)!),
66
+ } as BasetenSttOptions;
67
+ }
68
+
69
+ // eslint-disable-next-line
70
+ async _recognize(_: AudioBuffer): Promise<stt.SpeechEvent> {
71
+ throw new Error('Recognize is not supported on Baseten STT');
72
+ }
73
+
74
+ updateOptions(opts: Partial<BasetenSttOptions>) {
75
+ this.#opts = {
76
+ ...this.#opts,
77
+ ...opts,
78
+ audioLanguage:
79
+ opts.audioLanguage !== undefined
80
+ ? normalizeLanguage(opts.audioLanguage)
81
+ : this.#opts.audioLanguage,
82
+ };
83
+ }
84
+
85
+ stream(): SpeechStream {
86
+ return new SpeechStream(this, this.#opts);
87
+ }
88
+ }
89
+
90
+ export class SpeechStream extends stt.SpeechStream {
91
+ #opts: BasetenSttOptions;
92
+ #logger = log();
93
+ #speaking = false;
94
+ #requestId = '';
95
+ label = 'baseten.SpeechStream';
96
+
97
+ constructor(stt: STT, opts: BasetenSttOptions) {
98
+ super(stt, opts.sampleRate);
99
+ this.#opts = opts;
100
+ this.closed = false;
101
+ }
102
+
103
+ private getWsUrl(): string {
104
+ if (this.#opts.modelEndpoint) {
105
+ return this.#opts.modelEndpoint;
106
+ }
107
+ // Fallback to constructing URL from modelId (deprecated)
108
+ return `wss://model-${this.#opts.modelId}.api.baseten.co/environments/${this.#opts.environment}/websocket`;
109
+ }
110
+
111
+ protected async run() {
112
+ const maxRetry = 32;
113
+ let retries = 0;
114
+
115
+ while (!this.input.closed && !this.closed) {
116
+ const url = this.getWsUrl();
117
+ const headers = {
118
+ Authorization: `Api-Key ${this.#opts.apiKey}`,
119
+ };
120
+
121
+ const ws = new WebSocket(url, { headers });
122
+
123
+ try {
124
+ await new Promise((resolve, reject) => {
125
+ ws.on('open', resolve);
126
+ ws.on('error', (error) => reject(error));
127
+ ws.on('close', (code) => reject(`WebSocket returned ${code}`));
128
+ });
129
+
130
+ await this.#runWS(ws);
131
+ } catch (e) {
132
+ if (!this.closed && !this.input.closed) {
133
+ if (retries >= maxRetry) {
134
+ throw new Error(`failed to connect to Baseten after ${retries} attempts: ${e}`);
135
+ }
136
+
137
+ const delay = Math.min(retries * 5, 10);
138
+ retries++;
139
+
140
+ this.#logger.warn(
141
+ `failed to connect to Baseten, retrying in ${delay} seconds: ${e} (${retries}/${maxRetry})`,
142
+ );
143
+ await new Promise((resolve) => setTimeout(resolve, delay * 1000));
144
+ } else {
145
+ this.#logger.warn(
146
+ `Baseten disconnected, connection is closed: ${e} (inputClosed: ${this.input.closed}, isClosed: ${this.closed})`,
147
+ );
148
+ }
149
+ }
150
+ }
151
+
152
+ this.closed = true;
153
+ }
154
+
155
+ async #runWS(ws: WebSocket) {
156
+ let closing = false;
157
+
158
+ // Send initial metadata
159
+ // Note: Baseten server expects 'vad_params' and 'streaming_whisper_params' field names
160
+ // (not 'streaming_vad_config', 'streaming_params', 'whisper_params' as in older versions)
161
+ const metadata = {
162
+ vad_params: {
163
+ threshold: this.#opts.vadThreshold,
164
+ min_silence_duration_ms: this.#opts.vadMinSilenceDurationMs,
165
+ speech_pad_ms: this.#opts.vadSpeechPadMs,
166
+ },
167
+ streaming_whisper_params: {
168
+ encoding: this.#opts.encoding ?? 'pcm_s16le',
169
+ sample_rate: this.#opts.sampleRate ?? 16000,
170
+ enable_partial_transcripts: false,
171
+ audio_language: this.#opts.audioLanguage ?? 'en',
172
+ show_word_timestamps: true,
173
+ },
174
+ };
175
+
176
+ ws.send(JSON.stringify(metadata));
177
+
178
+ const sendTask = async () => {
179
+ const sampleRate = this.#opts.sampleRate ?? 16000;
180
+ const samplesPerChunk = sampleRate === 16000 ? 512 : 256;
181
+ const audioByteStream = new AudioByteStream(sampleRate, 1, samplesPerChunk);
182
+
183
+ try {
184
+ while (!this.closed) {
185
+ const result = await this.input.next();
186
+ if (result.done) {
187
+ break;
188
+ }
189
+
190
+ const data = result.value;
191
+
192
+ let frames: AudioFrame[];
193
+ if (data === SpeechStream.FLUSH_SENTINEL) {
194
+ // Flush any remaining buffered audio
195
+ frames = audioByteStream.flush();
196
+ } else {
197
+ if (data.sampleRate !== sampleRate || data.channels !== 1) {
198
+ throw new Error(
199
+ `sample rate or channel count mismatch: expected ${sampleRate}Hz/1ch, got ${data.sampleRate}Hz/${data.channels}ch`,
200
+ );
201
+ }
202
+ frames = audioByteStream.write(data.data.buffer as ArrayBuffer);
203
+ }
204
+
205
+ for (const frame of frames) {
206
+ const buffer = Buffer.from(
207
+ frame.data.buffer,
208
+ frame.data.byteOffset,
209
+ frame.data.byteLength,
210
+ );
211
+ ws.send(buffer);
212
+ }
213
+ }
214
+ } finally {
215
+ closing = true;
216
+ ws.close();
217
+ }
218
+ };
219
+
220
+ const listenTask = Task.from(async (controller) => {
221
+ const listenMessage = new Promise<void>((resolve, reject) => {
222
+ ws.on('message', (data) => {
223
+ try {
224
+ let jsonString: string;
225
+
226
+ if (typeof data === 'string') {
227
+ jsonString = data;
228
+ } else if (data instanceof Buffer) {
229
+ jsonString = data.toString('utf-8');
230
+ } else if (Array.isArray(data)) {
231
+ jsonString = Buffer.concat(data).toString('utf-8');
232
+ } else {
233
+ return;
234
+ }
235
+
236
+ const msg = JSON.parse(jsonString);
237
+ const isFinal = msg.is_final ?? true;
238
+ const segments = msg.segments ?? [];
239
+ const transcript = msg.transcript ?? '';
240
+ const confidence = msg.confidence ?? 0.0;
241
+ const languageCode = normalizeLanguage(msg.language_code ?? this.#opts.audioLanguage);
242
+
243
+ // Skip if no transcript text
244
+ if (!transcript) {
245
+ this.#logger.debug('Received non-transcript message:', msg);
246
+ return;
247
+ }
248
+
249
+ // Emit START_OF_SPEECH if not already speaking (only for interim or first final)
250
+ if (!this.#speaking && !isFinal) {
251
+ this.#speaking = true;
252
+ this.queue.put({ type: stt.SpeechEventType.START_OF_SPEECH });
253
+ }
254
+
255
+ // Note: Baseten uses 'start_time' and 'end_time' field names (with underscores)
256
+ const startTime =
257
+ segments.length > 0
258
+ ? (segments[0].start_time ?? 0.0) + this.startTimeOffset
259
+ : this.startTimeOffset;
260
+ const endTime =
261
+ segments.length > 0
262
+ ? (segments[segments.length - 1].end_time ?? 0.0) + this.startTimeOffset
263
+ : this.startTimeOffset;
264
+
265
+ // Note: Baseten returns segments (chunks) which we treat as words for aligned transcripts
266
+ const words = segments.map(
267
+ (segment: { text?: string; start_time?: number; end_time?: number }) => ({
268
+ text: segment.text ?? '',
269
+ startTime: (segment.start_time ?? 0.0) + this.startTimeOffset,
270
+ endTime: (segment.end_time ?? 0.0) + this.startTimeOffset,
271
+ startTimeOffset: this.startTimeOffset,
272
+ confidence: confidence,
273
+ }),
274
+ );
275
+
276
+ const speechData: stt.SpeechData = {
277
+ language: languageCode,
278
+ text: transcript,
279
+ startTime,
280
+ endTime,
281
+ confidence,
282
+ words: words.length > 0 ? words : undefined,
283
+ };
284
+
285
+ // Handle interim vs final transcripts (matching Python implementation)
286
+ if (!isFinal) {
287
+ // Interim transcript
288
+ this.queue.put({
289
+ type: stt.SpeechEventType.INTERIM_TRANSCRIPT,
290
+ alternatives: [speechData],
291
+ });
292
+ } else {
293
+ // Final transcript
294
+ this.queue.put({
295
+ type: stt.SpeechEventType.FINAL_TRANSCRIPT,
296
+ alternatives: [speechData],
297
+ });
298
+
299
+ // Emit END_OF_SPEECH after final transcript
300
+ if (this.#speaking) {
301
+ this.#speaking = false;
302
+ this.queue.put({ type: stt.SpeechEventType.END_OF_SPEECH });
303
+ }
304
+ }
305
+
306
+ if (this.closed || closing) {
307
+ resolve();
308
+ }
309
+ } catch (err) {
310
+ this.#logger.error(`STT: Error processing message: ${data}`);
311
+ reject(err);
312
+ }
313
+ });
314
+
315
+ ws.on('error', (err) => {
316
+ if (!closing) {
317
+ reject(err);
318
+ }
319
+ });
320
+
321
+ ws.on('close', () => {
322
+ if (!closing) {
323
+ resolve();
324
+ }
325
+ });
326
+ });
327
+
328
+ await Promise.race([listenMessage, waitForAbort(controller.signal)]);
329
+ }, this.abortController);
330
+
331
+ await Promise.all([sendTask(), listenTask.result]);
332
+ closing = true;
333
+ ws.close();
334
+ }
335
+ }
@@ -0,0 +1,21 @@
1
+ // SPDX-FileCopyrightText: 2024 LiveKit, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ import { tts } from '@livekit/agents-plugins-test';
5
+ import { describe, it } from 'vitest';
6
+ import { STT } from './stt.js';
7
+ import { TTS } from './tts.js';
8
+
9
+ const hasBasetenStreamingConfig = Boolean(
10
+ process.env.BASETEN_API_KEY && process.env.BASETEN_MODEL_ENDPOINT,
11
+ );
12
+
13
+ if (hasBasetenStreamingConfig) {
14
+ describe('Baseten', async () => {
15
+ await tts(new TTS(), new STT(), { streaming: false });
16
+ });
17
+ } else {
18
+ describe('Baseten', () => {
19
+ it.skip('requires Baseten streaming credentials/config', () => {});
20
+ });
21
+ }