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);
Copy link
Contributor

@toubatbrian toubatbrian Nov 6, 2025

Choose a reason for hiding this comment

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

What's this line for?

Copy link
Author

@simllll simllll Nov 6, 2025

Choose a reason for hiding this comment

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

It's a pattern I use all the time to make it easy to check of the instance of this error...e.g. error instanceOf ElevenlabsError, without this construct this doesn't evaluate to true.

Maybe this is not needed nowadays anymore,guess it depends on the transpile&runtime.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sounds good! Maybe add a 1 line comments to make it better understandable.

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"?

Copy link
Contributor

Choose a reason for hiding this comment

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

}
break;
}
Expand Down