Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tender-queens-drum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@livekit/agents-plugin-elevenlabs': patch
---

check for errors in elevenlabs
84 changes: 84 additions & 0 deletions plugins/elevenlabs/src/errors.ts
Original file line number Diff line number Diff line change
@@ -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');
}
1 change: 1 addition & 0 deletions plugins/elevenlabs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
13 changes: 12 additions & 1 deletion plugins/elevenlabs/src/tts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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'));
Expand All @@ -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;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could throw only on ElevenLabsError, but I can't see a reason why other errors should be "silently ignored"?

}
break;
}
Expand Down