@livekit/agents 1.0.22 → 1.0.23

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 (131) hide show
  1. package/dist/inference/api_protos.cjs +2 -2
  2. package/dist/inference/api_protos.cjs.map +1 -1
  3. package/dist/inference/api_protos.d.cts +16 -16
  4. package/dist/inference/api_protos.d.ts +16 -16
  5. package/dist/inference/api_protos.js +2 -2
  6. package/dist/inference/api_protos.js.map +1 -1
  7. package/dist/ipc/job_proc_lazy_main.cjs +35 -1
  8. package/dist/ipc/job_proc_lazy_main.cjs.map +1 -1
  9. package/dist/ipc/job_proc_lazy_main.js +13 -1
  10. package/dist/ipc/job_proc_lazy_main.js.map +1 -1
  11. package/dist/job.cjs +52 -6
  12. package/dist/job.cjs.map +1 -1
  13. package/dist/job.d.cts +2 -0
  14. package/dist/job.d.ts +2 -0
  15. package/dist/job.d.ts.map +1 -1
  16. package/dist/job.js +52 -6
  17. package/dist/job.js.map +1 -1
  18. package/dist/llm/llm.cjs +38 -3
  19. package/dist/llm/llm.cjs.map +1 -1
  20. package/dist/llm/llm.d.cts +1 -0
  21. package/dist/llm/llm.d.ts +1 -0
  22. package/dist/llm/llm.d.ts.map +1 -1
  23. package/dist/llm/llm.js +38 -3
  24. package/dist/llm/llm.js.map +1 -1
  25. package/dist/log.cjs +34 -10
  26. package/dist/log.cjs.map +1 -1
  27. package/dist/log.d.cts +7 -0
  28. package/dist/log.d.ts +7 -0
  29. package/dist/log.d.ts.map +1 -1
  30. package/dist/log.js +34 -11
  31. package/dist/log.js.map +1 -1
  32. package/dist/telemetry/index.cjs +23 -2
  33. package/dist/telemetry/index.cjs.map +1 -1
  34. package/dist/telemetry/index.d.cts +4 -1
  35. package/dist/telemetry/index.d.ts +4 -1
  36. package/dist/telemetry/index.d.ts.map +1 -1
  37. package/dist/telemetry/index.js +27 -2
  38. package/dist/telemetry/index.js.map +1 -1
  39. package/dist/telemetry/logging.cjs +65 -0
  40. package/dist/telemetry/logging.cjs.map +1 -0
  41. package/dist/telemetry/logging.d.cts +21 -0
  42. package/dist/telemetry/logging.d.ts +21 -0
  43. package/dist/telemetry/logging.d.ts.map +1 -0
  44. package/dist/telemetry/logging.js +40 -0
  45. package/dist/telemetry/logging.js.map +1 -0
  46. package/dist/telemetry/otel_http_exporter.cjs +144 -0
  47. package/dist/telemetry/otel_http_exporter.cjs.map +1 -0
  48. package/dist/telemetry/otel_http_exporter.d.cts +62 -0
  49. package/dist/telemetry/otel_http_exporter.d.ts +62 -0
  50. package/dist/telemetry/otel_http_exporter.d.ts.map +1 -0
  51. package/dist/telemetry/otel_http_exporter.js +120 -0
  52. package/dist/telemetry/otel_http_exporter.js.map +1 -0
  53. package/dist/telemetry/pino_otel_transport.cjs +217 -0
  54. package/dist/telemetry/pino_otel_transport.cjs.map +1 -0
  55. package/dist/telemetry/pino_otel_transport.d.cts +58 -0
  56. package/dist/telemetry/pino_otel_transport.d.ts +58 -0
  57. package/dist/telemetry/pino_otel_transport.d.ts.map +1 -0
  58. package/dist/telemetry/pino_otel_transport.js +189 -0
  59. package/dist/telemetry/pino_otel_transport.js.map +1 -0
  60. package/dist/telemetry/traces.cjs +225 -16
  61. package/dist/telemetry/traces.cjs.map +1 -1
  62. package/dist/telemetry/traces.d.cts +17 -0
  63. package/dist/telemetry/traces.d.ts +17 -0
  64. package/dist/telemetry/traces.d.ts.map +1 -1
  65. package/dist/telemetry/traces.js +211 -14
  66. package/dist/telemetry/traces.js.map +1 -1
  67. package/dist/tts/tts.cjs +62 -5
  68. package/dist/tts/tts.cjs.map +1 -1
  69. package/dist/tts/tts.d.cts +2 -0
  70. package/dist/tts/tts.d.ts +2 -0
  71. package/dist/tts/tts.d.ts.map +1 -1
  72. package/dist/tts/tts.js +62 -5
  73. package/dist/tts/tts.js.map +1 -1
  74. package/dist/utils.cjs +6 -0
  75. package/dist/utils.cjs.map +1 -1
  76. package/dist/utils.d.cts +1 -0
  77. package/dist/utils.d.ts +1 -0
  78. package/dist/utils.d.ts.map +1 -1
  79. package/dist/utils.js +5 -0
  80. package/dist/utils.js.map +1 -1
  81. package/dist/voice/agent_activity.cjs +93 -7
  82. package/dist/voice/agent_activity.cjs.map +1 -1
  83. package/dist/voice/agent_activity.d.cts +3 -0
  84. package/dist/voice/agent_activity.d.ts +3 -0
  85. package/dist/voice/agent_activity.d.ts.map +1 -1
  86. package/dist/voice/agent_activity.js +93 -7
  87. package/dist/voice/agent_activity.js.map +1 -1
  88. package/dist/voice/agent_session.cjs +122 -27
  89. package/dist/voice/agent_session.cjs.map +1 -1
  90. package/dist/voice/agent_session.d.cts +15 -0
  91. package/dist/voice/agent_session.d.ts +15 -0
  92. package/dist/voice/agent_session.d.ts.map +1 -1
  93. package/dist/voice/agent_session.js +122 -27
  94. package/dist/voice/agent_session.js.map +1 -1
  95. package/dist/voice/audio_recognition.cjs +69 -22
  96. package/dist/voice/audio_recognition.cjs.map +1 -1
  97. package/dist/voice/audio_recognition.d.cts +5 -0
  98. package/dist/voice/audio_recognition.d.ts +5 -0
  99. package/dist/voice/audio_recognition.d.ts.map +1 -1
  100. package/dist/voice/audio_recognition.js +69 -22
  101. package/dist/voice/audio_recognition.js.map +1 -1
  102. package/dist/voice/generation.cjs +43 -3
  103. package/dist/voice/generation.cjs.map +1 -1
  104. package/dist/voice/generation.d.ts.map +1 -1
  105. package/dist/voice/generation.js +43 -3
  106. package/dist/voice/generation.js.map +1 -1
  107. package/dist/voice/report.cjs +3 -2
  108. package/dist/voice/report.cjs.map +1 -1
  109. package/dist/voice/report.d.cts +7 -1
  110. package/dist/voice/report.d.ts +7 -1
  111. package/dist/voice/report.d.ts.map +1 -1
  112. package/dist/voice/report.js +3 -2
  113. package/dist/voice/report.js.map +1 -1
  114. package/package.json +8 -2
  115. package/src/inference/api_protos.ts +2 -2
  116. package/src/ipc/job_proc_lazy_main.ts +12 -1
  117. package/src/job.ts +59 -10
  118. package/src/llm/llm.ts +48 -5
  119. package/src/log.ts +52 -15
  120. package/src/telemetry/index.ts +22 -4
  121. package/src/telemetry/logging.ts +55 -0
  122. package/src/telemetry/otel_http_exporter.ts +191 -0
  123. package/src/telemetry/pino_otel_transport.ts +265 -0
  124. package/src/telemetry/traces.ts +320 -20
  125. package/src/tts/tts.ts +71 -9
  126. package/src/utils.ts +5 -0
  127. package/src/voice/agent_activity.ts +140 -22
  128. package/src/voice/agent_session.ts +174 -34
  129. package/src/voice/audio_recognition.ts +85 -26
  130. package/src/voice/generation.ts +59 -7
  131. package/src/voice/report.ts +10 -4
@@ -1,6 +1,7 @@
1
1
  // SPDX-FileCopyrightText: 2025 LiveKit, Inc.
2
2
  //
3
3
  // SPDX-License-Identifier: Apache-2.0
4
+ import { MetricsRecordingHeader } from '@livekit/protocol';
4
5
  import {
5
6
  type Attributes,
6
7
  type Context,
@@ -11,13 +12,20 @@ import {
11
12
  context as otelContext,
12
13
  trace,
13
14
  } from '@opentelemetry/api';
14
- import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
15
+ import { SeverityNumber } from '@opentelemetry/api-logs';
16
+ import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';
15
17
  import { CompressionAlgorithm } from '@opentelemetry/otlp-exporter-base';
16
18
  import { Resource } from '@opentelemetry/resources';
17
19
  import type { ReadableSpan, SpanProcessor } from '@opentelemetry/sdk-trace-base';
18
20
  import { BatchSpanProcessor, NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
19
21
  import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
22
+ import FormData from 'form-data';
20
23
  import { AccessToken } from 'livekit-server-sdk';
24
+ import type { ChatContent, ChatItem } from '../llm/index.js';
25
+ import { enableOtelLogging } from '../log.js';
26
+ import type { SessionReport } from '../voice/report.js';
27
+ import { type SimpleLogRecord, SimpleOTLPHttpLogExporter } from './otel_http_exporter.js';
28
+ import { flushPinoLogs, initPinoCloudExporter } from './pino_otel_transport.js';
21
29
 
22
30
  export interface StartSpanOptions {
23
31
  /** Name of the span */
@@ -94,19 +102,15 @@ class DynamicTracer {
94
102
  const endOnExit = options.endOnExit === undefined ? true : options.endOnExit; // default true
95
103
  const opts: SpanOptions = { attributes: options.attributes };
96
104
 
97
- return new Promise((resolve, reject) => {
98
- this.tracer.startActiveSpan(options.name, opts, ctx, async (span) => {
99
- try {
100
- const result = await fn(span);
101
- resolve(result);
102
- } catch (error) {
103
- reject(error);
104
- } finally {
105
- if (endOnExit) {
106
- span.end();
107
- }
105
+ // Directly return the tracer's startActiveSpan result - it handles async correctly
106
+ return await this.tracer.startActiveSpan(options.name, opts, ctx, async (span) => {
107
+ try {
108
+ return await fn(span);
109
+ } finally {
110
+ if (endOnExit) {
111
+ span.end();
108
112
  }
109
- });
113
+ }
110
114
  });
111
115
  }
112
116
 
@@ -162,10 +166,6 @@ class MetadataSpanProcessor implements SpanProcessor {
162
166
  }
163
167
  }
164
168
 
165
- // TODO(brian): PR4 - Add MetadataLogProcessor for structured logging
166
-
167
- // TODO(brian): PR4 - Add ExtraDetailsProcessor for structured logging
168
-
169
169
  /**
170
170
  * Set the tracer provider for the livekit-agents framework.
171
171
  * This should be called before agent session start if using custom tracer providers.
@@ -254,13 +254,313 @@ export async function setupCloudTracer(options: {
254
254
  });
255
255
  tracerProvider.register();
256
256
 
257
- // Metadata processor is already configured in the constructor above
258
257
  setTracerProvider(tracerProvider);
259
258
 
260
- // TODO(brian): PR4 - Add logger provider setup here for structured logging
261
- // Similar to Python's setup: LoggerProvider, OTLPLogExporter, BatchLogRecordProcessor
259
+ // Initialize standalone Pino cloud exporter (no OTEL SDK dependency)
260
+ initPinoCloudExporter({
261
+ cloudHostname,
262
+ roomId,
263
+ jobId,
264
+ });
265
+
266
+ enableOtelLogging();
262
267
  } catch (error) {
263
268
  console.error('Failed to setup cloud tracer:', error);
264
269
  throw error;
265
270
  }
266
271
  }
272
+
273
+ /**
274
+ * Flush all pending Pino logs to ensure they are exported.
275
+ * Call this before session/job ends to ensure all logs are sent.
276
+ *
277
+ * @internal
278
+ */
279
+ export async function flushOtelLogs(): Promise<void> {
280
+ await flushPinoLogs();
281
+ }
282
+
283
+ /**
284
+ * Convert ChatItem to proto-compatible dictionary format.
285
+ * TODO: Use actual agent_session proto types once @livekit/protocol v1.43.1+ is published
286
+ */
287
+ function chatItemToProto(item: ChatItem): Record<string, any> {
288
+ const itemDict: Record<string, any> = {};
289
+
290
+ if (item.type === 'message') {
291
+ const roleMap: Record<string, string> = {
292
+ developer: 'DEVELOPER',
293
+ system: 'SYSTEM',
294
+ user: 'USER',
295
+ assistant: 'ASSISTANT',
296
+ };
297
+
298
+ const msg: Record<string, any> = {
299
+ id: item.id,
300
+ role: roleMap[item.role] || item.role.toUpperCase(),
301
+ content: item.content.map((c: ChatContent) => ({ text: c })),
302
+ createdAt: toRFC3339(item.createdAt),
303
+ };
304
+
305
+ if (item.interrupted) {
306
+ msg.interrupted = item.interrupted;
307
+ }
308
+
309
+ // TODO(brian): Add extra and transcriptConfidence to ChatMessage
310
+ // if (item.extra && Object.keys(item.extra).length > 0) {
311
+ // msg.extra = item.extra;
312
+ // }
313
+
314
+ // if (item.transcriptConfidence !== undefined && item.transcriptConfidence !== null) {
315
+ // msg.transcriptConfidence = item.transcriptConfidence;
316
+ // }
317
+
318
+ // TODO(brian): Add metrics to ChatMessage
319
+ // const metrics = item.metrics || {};
320
+ // if (Object.keys(metrics).length > 0) {
321
+ // msg.metrics = {};
322
+ // if (metrics.started_speaking_at) {
323
+ // msg.metrics.startedSpeakingAt = toRFC3339(metrics.started_speaking_at);
324
+ // }
325
+ // if (metrics.stopped_speaking_at) {
326
+ // msg.metrics.stoppedSpeakingAt = toRFC3339(metrics.stopped_speaking_at);
327
+ // }
328
+ // if (metrics.transcription_delay !== undefined) {
329
+ // msg.metrics.transcriptionDelay = metrics.transcription_delay;
330
+ // }
331
+ // if (metrics.end_of_turn_delay !== undefined) {
332
+ // msg.metrics.endOfTurnDelay = metrics.end_of_turn_delay;
333
+ // }
334
+ // if (metrics.on_user_turn_completed_delay !== undefined) {
335
+ // msg.metrics.onUserTurnCompletedDelay = metrics.on_user_turn_completed_delay;
336
+ // }
337
+ // if (metrics.llm_node_ttft !== undefined) {
338
+ // msg.metrics.llmNodeTtft = metrics.llm_node_ttft;
339
+ // }
340
+ // if (metrics.tts_node_ttfb !== undefined) {
341
+ // msg.metrics.ttsNodeTtfb = metrics.tts_node_ttfb;
342
+ // }
343
+ // if (metrics.e2e_latency !== undefined) {
344
+ // msg.metrics.e2eLatency = metrics.e2e_latency;
345
+ // }
346
+ // }
347
+
348
+ itemDict.message = msg;
349
+ } else if (item.type === 'function_call') {
350
+ itemDict.functionCall = {
351
+ id: item.id,
352
+ callId: item.callId,
353
+ arguments: item.args,
354
+ name: item.name,
355
+ createdAt: toRFC3339(item.createdAt),
356
+ };
357
+ } else if (item.type === 'function_call_output') {
358
+ itemDict.functionCallOutput = {
359
+ id: item.id,
360
+ name: item.name,
361
+ callId: item.callId,
362
+ output: item.output,
363
+ isError: item.isError,
364
+ createdAt: toRFC3339(item.createdAt),
365
+ };
366
+ } else if (item.type === 'agent_handoff') {
367
+ const handoff: Record<string, any> = {
368
+ id: item.id,
369
+ newAgentId: item.newAgentId,
370
+ createdAt: toRFC3339(item.createdAt),
371
+ };
372
+ if (item.oldAgentId !== undefined && item.oldAgentId !== null && item.oldAgentId !== '') {
373
+ handoff.oldAgentId = item.oldAgentId;
374
+ }
375
+ itemDict.agentHandoff = handoff;
376
+ }
377
+
378
+ try {
379
+ if (item.type === 'function_call' && typeof itemDict.functionCall?.arguments === 'string') {
380
+ itemDict.functionCall.arguments = JSON.parse(itemDict.functionCall.arguments);
381
+ } else if (
382
+ item.type === 'function_call_output' &&
383
+ typeof itemDict.functionCallOutput?.output === 'string'
384
+ ) {
385
+ itemDict.functionCallOutput.output = JSON.parse(itemDict.functionCallOutput.output);
386
+ }
387
+ } catch {
388
+ // ignore parsing errors
389
+ }
390
+
391
+ return itemDict;
392
+ }
393
+
394
+ /**
395
+ * Convert timestamp to RFC3339 format matching Python's _to_rfc3339.
396
+ * Note: TypeScript createdAt is in milliseconds (Date.now()), not seconds like Python.
397
+ * @internal
398
+ */
399
+ function toRFC3339(valueMs: number | Date): string {
400
+ // valueMs is already in milliseconds (from Date.now())
401
+ const dt = valueMs instanceof Date ? valueMs : new Date(valueMs);
402
+ // Truncate sub-millisecond precision
403
+ const truncated = new Date(Math.floor(dt.getTime()));
404
+ return truncated.toISOString();
405
+ }
406
+
407
+ /**
408
+ * Upload session report to LiveKit Cloud observability.
409
+ * @param options - Configuration with agentName, cloudHostname, and report
410
+ */
411
+ export async function uploadSessionReport(options: {
412
+ agentName: string;
413
+ cloudHostname: string;
414
+ report: SessionReport;
415
+ }): Promise<void> {
416
+ const { agentName, cloudHostname, report } = options;
417
+
418
+ // Create OTLP HTTP exporter for chat history logs
419
+ // Uses raw HTTP JSON format which is required by LiveKit Cloud
420
+ const logExporter = new SimpleOTLPHttpLogExporter({
421
+ cloudHostname,
422
+ resourceAttributes: {
423
+ room_id: report.roomId,
424
+ job_id: report.jobId,
425
+ },
426
+ scopeName: 'chat_history',
427
+ scopeAttributes: {
428
+ room_id: report.roomId,
429
+ job_id: report.jobId,
430
+ room: report.room,
431
+ },
432
+ });
433
+
434
+ // Build log records for session report and chat items
435
+ const logRecords: SimpleLogRecord[] = [];
436
+
437
+ const commonAttrs = {
438
+ room_id: report.roomId,
439
+ job_id: report.jobId,
440
+ 'logger.name': 'chat_history',
441
+ };
442
+
443
+ logRecords.push({
444
+ body: 'session report',
445
+ timestampMs: report.startedAt || report.timestamp || 0,
446
+ attributes: {
447
+ ...commonAttrs,
448
+ 'session.options': report.options || {},
449
+ 'session.report_timestamp': report.timestamp,
450
+ agent_name: agentName,
451
+ },
452
+ });
453
+
454
+ // Track last timestamp to ensure monotonic ordering when items have identical timestamps
455
+ // This fixes the issue where function_call and function_call_output with same timestamp
456
+ // get reordered by the dashboard
457
+ let lastTimestamp = 0;
458
+ for (const item of report.chatHistory.items) {
459
+ // Ensure monotonically increasing timestamps for proper ordering
460
+ // Add 0.001ms (1 microsecond) offset when timestamps collide
461
+ let itemTimestamp = item.createdAt;
462
+ if (itemTimestamp <= lastTimestamp) {
463
+ itemTimestamp = lastTimestamp + 0.001; // Add 1 microsecond
464
+ }
465
+ lastTimestamp = itemTimestamp;
466
+
467
+ const itemProto = chatItemToProto(item);
468
+ let severityNumber = SeverityNumber.UNSPECIFIED;
469
+ let severityText = 'unspecified';
470
+
471
+ if (item.type === 'function_call_output' && item.isError) {
472
+ severityNumber = SeverityNumber.ERROR;
473
+ severityText = 'error';
474
+ }
475
+
476
+ logRecords.push({
477
+ body: 'chat item',
478
+ timestampMs: itemTimestamp, // Adjusted for monotonic ordering
479
+ attributes: { 'chat.item': itemProto, ...commonAttrs },
480
+ severityNumber,
481
+ severityText,
482
+ });
483
+ }
484
+ await logExporter.export(logRecords);
485
+
486
+ const apiKey = process.env.LIVEKIT_API_KEY;
487
+ const apiSecret = process.env.LIVEKIT_API_SECRET;
488
+
489
+ if (!apiKey || !apiSecret) {
490
+ throw new Error('LIVEKIT_API_KEY and LIVEKIT_API_SECRET must be set for session upload');
491
+ }
492
+
493
+ const token = new AccessToken(apiKey, apiSecret, { ttl: '6h' });
494
+ token.addObservabilityGrant({ write: true });
495
+ const jwt = await token.toJwt();
496
+
497
+ const formData = new FormData();
498
+
499
+ // Add header (protobuf MetricsRecordingHeader)
500
+ const headerMsg = new MetricsRecordingHeader({
501
+ roomId: report.roomId,
502
+ duration: BigInt(0), // TODO: Calculate actual duration from report
503
+ startTime: {
504
+ seconds: BigInt(Math.floor(report.timestamp / 1000)),
505
+ nanos: Math.floor((report.timestamp % 1000) * 1e6),
506
+ },
507
+ });
508
+
509
+ const headerBytes = Buffer.from(headerMsg.toBinary());
510
+ formData.append('header', headerBytes, {
511
+ filename: 'header.binpb',
512
+ contentType: 'application/protobuf',
513
+ knownLength: headerBytes.length,
514
+ header: {
515
+ 'Content-Type': 'application/protobuf',
516
+ 'Content-Length': headerBytes.length.toString(),
517
+ },
518
+ });
519
+
520
+ // Add chat_history JSON
521
+ const chatHistoryJson = JSON.stringify(report.chatHistory.toJSON({ excludeTimestamp: false }));
522
+ const chatHistoryBuffer = Buffer.from(chatHistoryJson, 'utf-8');
523
+ formData.append('chat_history', chatHistoryBuffer, {
524
+ filename: 'chat_history.json',
525
+ contentType: 'application/json',
526
+ knownLength: chatHistoryBuffer.length,
527
+ header: {
528
+ 'Content-Type': 'application/json',
529
+ 'Content-Length': chatHistoryBuffer.length.toString(),
530
+ },
531
+ });
532
+
533
+ // TODO(brian): Add audio recording file when recorder IO is implemented
534
+
535
+ // Upload to LiveKit Cloud using form-data's submit method
536
+ // This properly streams the multipart form with all headers including Content-Length
537
+ return new Promise<void>((resolve, reject) => {
538
+ formData.submit(
539
+ {
540
+ protocol: 'https:',
541
+ host: cloudHostname,
542
+ path: '/observability/recordings/v0',
543
+ method: 'POST',
544
+ headers: {
545
+ Authorization: `Bearer ${jwt}`,
546
+ },
547
+ },
548
+ (err, res) => {
549
+ if (err) {
550
+ reject(new Error(`Failed to upload session report: ${err.message}`));
551
+ return;
552
+ }
553
+
554
+ if (res.statusCode && res.statusCode >= 400) {
555
+ reject(
556
+ new Error(`Failed to upload session report: ${res.statusCode} ${res.statusMessage}`),
557
+ );
558
+ return;
559
+ }
560
+
561
+ res.resume(); // Drain the response
562
+ res.on('end', () => resolve());
563
+ },
564
+ );
565
+ });
566
+ }
package/src/tts/tts.ts CHANGED
@@ -3,12 +3,14 @@
3
3
  // SPDX-License-Identifier: Apache-2.0
4
4
  import type { AudioFrame } from '@livekit/rtc-node';
5
5
  import type { TypedEventEmitter as TypedEmitter } from '@livekit/typed-emitter';
6
+ import type { Span } from '@opentelemetry/api';
6
7
  import { EventEmitter } from 'node:events';
7
8
  import type { ReadableStream } from 'node:stream/web';
8
9
  import { APIConnectionError, APIError } from '../_exceptions.js';
9
10
  import { log } from '../log.js';
10
11
  import type { TTSMetrics } from '../metrics/base.js';
11
12
  import { DeferredReadableStream } from '../stream/deferred_stream.js';
13
+ import { recordException, traceTypes, tracer } from '../telemetry/index.js';
12
14
  import { type APIConnectOptions, DEFAULT_API_CONNECT_OPTIONS } from '../types.js';
13
15
  import { AsyncIterableQueue, delay, mergeFrames, startSoon, toError } from '../utils.js';
14
16
 
@@ -134,6 +136,7 @@ export abstract class SynthesizeStream
134
136
  #monitorMetricsTask?: Promise<void>;
135
137
  private _connOptions: APIConnectOptions;
136
138
  protected abortController = new AbortController();
139
+ #ttsRequestSpan?: Span;
137
140
 
138
141
  private deferredInputStream: DeferredReadableStream<
139
142
  string | typeof SynthesizeStream.FLUSH_SENTINEL
@@ -160,12 +163,27 @@ export abstract class SynthesizeStream
160
163
  startSoon(() => this.mainTask().then(() => this.queue.close()));
161
164
  }
162
165
 
163
- private async mainTask() {
164
- // TODO(brian): PR3 - Add span wrapping: tracer.startActiveSpan('tts_request', ..., { endOnExit: false })
166
+ private _mainTaskImpl = async (span: Span) => {
167
+ this.#ttsRequestSpan = span;
168
+ span.setAttributes({
169
+ [traceTypes.ATTR_TTS_STREAMING]: true,
170
+ [traceTypes.ATTR_TTS_LABEL]: this.#tts.label,
171
+ });
172
+
165
173
  for (let i = 0; i < this._connOptions.maxRetry + 1; i++) {
166
174
  try {
167
- // TODO(brian): PR3 - Add span for retry attempts: tracer.startActiveSpan('tts_request_run', ...)
168
- return await this.run();
175
+ return await tracer.startActiveSpan(
176
+ async (attemptSpan) => {
177
+ attemptSpan.setAttribute(traceTypes.ATTR_RETRY_COUNT, i);
178
+ try {
179
+ return await this.run();
180
+ } catch (error) {
181
+ recordException(attemptSpan, toError(error));
182
+ throw error;
183
+ }
184
+ },
185
+ { name: 'tts_request_run' },
186
+ );
169
187
  } catch (error) {
170
188
  if (error instanceof APIError) {
171
189
  const retryInterval = this._connOptions._intervalForRetry(i);
@@ -197,7 +215,13 @@ export abstract class SynthesizeStream
197
215
  }
198
216
  }
199
217
  }
200
- }
218
+ };
219
+
220
+ private mainTask = async () =>
221
+ tracer.startActiveSpan(async (span) => this._mainTaskImpl(span), {
222
+ name: 'tts_request',
223
+ endOnExit: false,
224
+ });
201
225
 
202
226
  private emitError({ error, recoverable }: { error: Error; recoverable: boolean }) {
203
227
  this.#tts.emit('error', {
@@ -265,6 +289,9 @@ export abstract class SynthesizeStream
265
289
  label: this.#tts.label,
266
290
  streamed: false,
267
291
  };
292
+ if (this.#ttsRequestSpan) {
293
+ this.#ttsRequestSpan.setAttribute(traceTypes.ATTR_TTS_METRICS, JSON.stringify(metrics));
294
+ }
268
295
  this.#tts.emit('metrics_collected', metrics);
269
296
  }
270
297
  };
@@ -289,6 +316,11 @@ export abstract class SynthesizeStream
289
316
  if (requestId) {
290
317
  emit();
291
318
  }
319
+
320
+ if (this.#ttsRequestSpan) {
321
+ this.#ttsRequestSpan.end();
322
+ this.#ttsRequestSpan = undefined;
323
+ }
292
324
  }
293
325
 
294
326
  protected abstract run(): Promise<void>;
@@ -377,6 +409,7 @@ export abstract class ChunkedStream implements AsyncIterableIterator<Synthesized
377
409
  abstract label: string;
378
410
  #text: string;
379
411
  #tts: TTS;
412
+ #ttsRequestSpan?: Span;
380
413
  private _connOptions: APIConnectOptions;
381
414
  private logger = log();
382
415
 
@@ -398,12 +431,27 @@ export abstract class ChunkedStream implements AsyncIterableIterator<Synthesized
398
431
  Promise.resolve().then(() => this.mainTask().then(() => this.queue.close()));
399
432
  }
400
433
 
401
- private async mainTask() {
402
- // TODO(brian): PR3 - Add span wrapping: tracer.startActiveSpan('tts_request', ..., { endOnExit: false })
434
+ private _mainTaskImpl = async (span: Span) => {
435
+ this.#ttsRequestSpan = span;
436
+ span.setAttributes({
437
+ [traceTypes.ATTR_TTS_STREAMING]: false,
438
+ [traceTypes.ATTR_TTS_LABEL]: this.#tts.label,
439
+ });
440
+
403
441
  for (let i = 0; i < this._connOptions.maxRetry + 1; i++) {
404
442
  try {
405
- // TODO(brian): PR3 - Add span for retry attempts: tracer.startActiveSpan('tts_request_run', ...)
406
- return await this.run();
443
+ return await tracer.startActiveSpan(
444
+ async (attemptSpan) => {
445
+ attemptSpan.setAttribute(traceTypes.ATTR_RETRY_COUNT, i);
446
+ try {
447
+ return await this.run();
448
+ } catch (error) {
449
+ recordException(attemptSpan, toError(error));
450
+ throw error;
451
+ }
452
+ },
453
+ { name: 'tts_request_run' },
454
+ );
407
455
  } catch (error) {
408
456
  if (error instanceof APIError) {
409
457
  const retryInterval = this._connOptions._intervalForRetry(i);
@@ -435,6 +483,13 @@ export abstract class ChunkedStream implements AsyncIterableIterator<Synthesized
435
483
  }
436
484
  }
437
485
  }
486
+ };
487
+
488
+ private async mainTask() {
489
+ return tracer.startActiveSpan(async (span) => this._mainTaskImpl(span), {
490
+ name: 'tts_request',
491
+ endOnExit: false,
492
+ });
438
493
  }
439
494
 
440
495
  private emitError({ error, recoverable }: { error: Error; recoverable: boolean }) {
@@ -482,6 +537,13 @@ export abstract class ChunkedStream implements AsyncIterableIterator<Synthesized
482
537
  label: this.#tts.label,
483
538
  streamed: false,
484
539
  };
540
+
541
+ if (this.#ttsRequestSpan) {
542
+ this.#ttsRequestSpan.setAttribute(traceTypes.ATTR_TTS_METRICS, JSON.stringify(metrics));
543
+ this.#ttsRequestSpan.end();
544
+ this.#ttsRequestSpan = undefined;
545
+ }
546
+
485
547
  this.#tts.emit('metrics_collected', metrics);
486
548
  }
487
549
 
package/src/utils.ts CHANGED
@@ -839,3 +839,8 @@ export async function waitForAbort(signal: AbortSignal) {
839
839
  signal.addEventListener('abort', handler, { once: true });
840
840
  return await abortFuture.await;
841
841
  }
842
+
843
+ export const isCloud = (url: URL) => {
844
+ const hostname = url.hostname;
845
+ return hostname.endsWith('.livekit.cloud') || hostname.endsWith('.livekit.run');
846
+ };