diff --git a/.changeset/tender-queens-drum.md b/.changeset/tender-queens-drum.md new file mode 100644 index 000000000..9ee3f1919 --- /dev/null +++ b/.changeset/tender-queens-drum.md @@ -0,0 +1,5 @@ +--- +'@livekit/agents-plugin-elevenlabs': patch +--- + +check for errors in elevenlabs diff --git a/plugins/elevenlabs/src/errors.ts b/plugins/elevenlabs/src/errors.ts new file mode 100644 index 000000000..eb19ed09a --- /dev/null +++ b/plugins/elevenlabs/src/errors.ts @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: 2025 LiveKit, Inc. +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * Base error class for ElevenLabs-specific exceptions + */ +export class ElevenLabsError extends Error { + public readonly statusCode?: number; + public readonly body?: unknown; + + constructor({ + message, + statusCode, + body, + }: { + message?: string; + statusCode?: number; + body?: unknown; + }) { + super(buildMessage({ message, statusCode, body })); + Object.setPrototypeOf(this, ElevenLabsError.prototype); + this.statusCode = statusCode; + this.body = body; + } +} + +/** + * Error thrown when a request to ElevenLabs times out + */ +export class ElevenLabsTimeoutError extends Error { + constructor(message: string) { + super(message); + Object.setPrototypeOf(this, ElevenLabsTimeoutError.prototype); + } +} + +/** + * Error thrown when WebSocket connection fails + */ +export class ElevenLabsConnectionError extends ElevenLabsError { + public readonly retries: number; + + constructor({ + message, + retries, + statusCode, + body, + }: { + message?: string; + retries: number; + statusCode?: number; + body?: unknown; + }) { + super({ message, statusCode, body }); + Object.setPrototypeOf(this, ElevenLabsConnectionError.prototype); + this.retries = retries; + } +} + +function buildMessage({ + message, + statusCode, + body, +}: { + message: string | undefined; + statusCode: number | undefined; + body: unknown | undefined; +}): string { + const lines: string[] = []; + if (message != null) { + lines.push(message); + } + + if (statusCode != null) { + lines.push(`Status code: ${statusCode.toString()}`); + } + + if (body != null) { + lines.push(`Body: ${JSON.stringify(body, undefined, 2)}`); + } + + return lines.join('\n'); +} diff --git a/plugins/elevenlabs/src/index.ts b/plugins/elevenlabs/src/index.ts index 66c4eeff6..68b82c31c 100644 --- a/plugins/elevenlabs/src/index.ts +++ b/plugins/elevenlabs/src/index.ts @@ -3,6 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 import { Plugin } from '@livekit/agents'; +export * from './errors.js'; export * from './tts.js'; class ElevenLabsPlugin extends Plugin { diff --git a/plugins/elevenlabs/src/tts.ts b/plugins/elevenlabs/src/tts.ts index 31793f981..246931ec5 100644 --- a/plugins/elevenlabs/src/tts.ts +++ b/plugins/elevenlabs/src/tts.ts @@ -12,6 +12,7 @@ import { import type { AudioFrame } from '@livekit/rtc-node'; import { URL } from 'node:url'; import { type RawData, WebSocket } from 'ws'; +import { ElevenLabsConnectionError, ElevenLabsError } from './errors.js'; import type { TTSEncoding, TTSModels } from './models.js'; const DEFAULT_INACTIVITY_TIMEOUT = 300; @@ -213,7 +214,10 @@ export class SynthesizeStream extends tts.SynthesizeStream { break; } catch (e) { if (retries >= maxRetry) { - throw new Error(`failed to connect to ElevenLabs after ${retries} attempts: ${e}`); + throw new ElevenLabsConnectionError({ + message: `Failed to connect to ElevenLabs after ${retries} attempts: ${e}`, + retries, + }); } const delay = Math.min(retries * 5, 5); @@ -298,6 +302,12 @@ export class SynthesizeStream extends tts.SynthesizeStream { }); }).then((msg) => { const json = JSON.parse(msg.toString()); + if (json.error) { + throw new ElevenLabsError({ + message: json.error, + body: json, + }); + } // remove the "audio" field from the json object when printing if ('audio' in json && json.audio !== null) { const data = new Int8Array(Buffer.from(json.audio, 'base64')); @@ -324,6 +334,7 @@ export class SynthesizeStream extends tts.SynthesizeStream { // skip log error for normal websocket close if (err instanceof Error && !err.message.includes('WebSocket closed')) { this.#logger.error({ err }, 'Error in listenTask from ElevenLabs WebSocket'); + throw err; } break; }