Skip to content

Commit 502a2b8

Browse files
fix(google): surface context exhaustion errors
1 parent e83def4 commit 502a2b8

2 files changed

Lines changed: 64 additions & 16 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@livekit/agents-plugin-google': patch
3+
---
4+
5+
Surface Gemini Live `1007` context exhaustion errors as unrecoverable session errors instead of retrying the same oversized context.

plugins/google/src/realtime/realtime_api.ts

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ const LK_GOOGLE_DEBUG = Number(process.env.LK_GOOGLE_DEBUG ?? 0);
4949

5050
// WebSocket close codes (RFC 6455)
5151
const WS_CLOSE_NORMAL = 1000;
52+
const WS_CLOSE_CONTEXT_EXHAUSTED = 1007;
5253
/**
5354
* Default image encoding options for Google Realtime API
5455
*/
@@ -465,6 +466,7 @@ export class RealtimeSession extends llm.RealtimeSession {
465466
private inUserActivity = false;
466467
private sessionLock = new Mutex();
467468
private numRetries = 0;
469+
private sessionError?: Error;
468470
private hasReceivedAudioInput = false;
469471
private pendingInterruptText = false;
470472
private earlyCompletionPending = false;
@@ -549,6 +551,20 @@ export class RealtimeSession extends llm.RealtimeSession {
549551
}
550552
}
551553

554+
private toError(error: unknown): Error {
555+
return error instanceof Error ? error : new Error(String(error));
556+
}
557+
558+
private isContextExhaustedError(error: unknown): boolean {
559+
return (
560+
(typeof error === 'object' &&
561+
error !== null &&
562+
'statusCode' in error &&
563+
error.statusCode === WS_CLOSE_CONTEXT_EXHAUSTED) ||
564+
String(error).includes(String(WS_CLOSE_CONTEXT_EXHAUSTED))
565+
);
566+
}
567+
552568
private isNonBlockingToolBehavior(): boolean {
553569
return this.options.toolBehavior === types.Behavior.NON_BLOCKING;
554570
}
@@ -1012,19 +1028,23 @@ export class RealtimeSession extends llm.RealtimeSession {
10121028
const errorMsg = event.reason || `WebSocket closed with code ${event.code}`;
10131029
this.#logger.error(`Gemini Live session error: ${errorMsg}${truncationNote}`);
10141030

1015-
this.emitError(
1016-
new APIStatusError({
1017-
message: `${errorMsg}${truncationNote}`,
1018-
options: {
1019-
statusCode: event.code,
1020-
retryable: false,
1021-
body: event.reason
1022-
? { reason: event.reason, code: event.code, truncated: isTruncated }
1023-
: null,
1024-
},
1025-
}),
1026-
false,
1027-
);
1031+
const error = new APIStatusError({
1032+
message: `${errorMsg}${truncationNote}`,
1033+
options: {
1034+
statusCode: event.code,
1035+
retryable: false,
1036+
body: event.reason
1037+
? { reason: event.reason, code: event.code, truncated: isTruncated }
1038+
: null,
1039+
},
1040+
});
1041+
1042+
if (event.code === WS_CLOSE_CONTEXT_EXHAUSTED) {
1043+
this.sessionError = error;
1044+
this.markRestartNeeded();
1045+
} else {
1046+
this.emitError(error, false);
1047+
}
10281048
} else {
10291049
this.#logger.debug('Gemini Live session closed:', event.code, event.reason);
10301050
}
@@ -1073,20 +1093,41 @@ export class RealtimeSession extends llm.RealtimeSession {
10731093
}
10741094

10751095
await cancelAndWait([sendTask, restartWaitTask], 2000);
1096+
1097+
if (this.sessionError) {
1098+
const error = this.sessionError;
1099+
this.sessionError = undefined;
1100+
throw error;
1101+
}
10761102
} catch (error) {
1077-
this.#logger.error(`Gemini Realtime API error: ${error}`);
1103+
const err = this.toError(error);
1104+
this.#logger.error(`Gemini Realtime API error: ${err}`);
10781105

10791106
if (this.#closed) break;
10801107

1108+
// Gemini Live closes with 1007 when the session context is exhausted. Reconnecting
1109+
// would replay the same oversized context and fail again, so terminate the session.
1110+
if (this.isContextExhaustedError(err)) {
1111+
this.#logger.error(
1112+
err,
1113+
'Gemini Live closed the session: context exhausted (1007). Reconnecting would replay the same context and fail again; terminating the session.',
1114+
);
1115+
this.emitError(err, false);
1116+
throw new APIConnectionError({
1117+
message: 'Gemini Live session context exhausted (1007)',
1118+
options: { retryable: false },
1119+
});
1120+
}
1121+
10811122
if (maxRetries === 0) {
1082-
this.emitError(error as Error, false);
1123+
this.emitError(err, false);
10831124
throw new APIConnectionError({
10841125
message: 'Failed to connect to Gemini Live',
10851126
});
10861127
}
10871128

10881129
if (this.numRetries >= maxRetries) {
1089-
this.emitError(error as Error, false);
1130+
this.emitError(err, false);
10901131
throw new APIConnectionError({
10911132
message: `Failed to connect to Gemini Live after ${maxRetries} attempts`,
10921133
});
@@ -1179,6 +1220,7 @@ export class RealtimeSession extends llm.RealtimeSession {
11791220
} catch (e) {
11801221
if (!this.sessionShouldClose.isSet) {
11811222
this.#logger.error(`Error in send task: ${e}`);
1223+
this.sessionError = this.toError(e);
11821224
this.markRestartNeeded();
11831225
}
11841226
} finally {
@@ -1292,6 +1334,7 @@ export class RealtimeSession extends llm.RealtimeSession {
12921334
} catch (e) {
12931335
if (!this.sessionShouldClose.isSet) {
12941336
this.#logger.error(`Error in onReceiveMessage: ${e}`);
1337+
this.sessionError = this.toError(e);
12951338
this.markRestartNeeded();
12961339
}
12971340
}

0 commit comments

Comments
 (0)