Skip to content

Commit b8d5af0

Browse files
fix(google): surface context exhaustion errors
1 parent 1bbbb3f commit b8d5af0

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
*/
@@ -473,6 +474,7 @@ export class RealtimeSession extends llm.RealtimeSession {
473474
private inUserActivity = false;
474475
private sessionLock = new Mutex();
475476
private numRetries = 0;
477+
private sessionError?: Error;
476478
private hasReceivedAudioInput = false;
477479
private pendingInterruptText = false;
478480
private earlyCompletionPending = false;
@@ -557,6 +559,20 @@ export class RealtimeSession extends llm.RealtimeSession {
557559
}
558560
}
559561

562+
private toError(error: unknown): Error {
563+
return error instanceof Error ? error : new Error(String(error));
564+
}
565+
566+
private isContextExhaustedError(error: unknown): boolean {
567+
return (
568+
(typeof error === 'object' &&
569+
error !== null &&
570+
'statusCode' in error &&
571+
error.statusCode === WS_CLOSE_CONTEXT_EXHAUSTED) ||
572+
String(error).includes(String(WS_CLOSE_CONTEXT_EXHAUSTED))
573+
);
574+
}
575+
560576
private isNonBlockingToolBehavior(): boolean {
561577
return this.options.toolBehavior === types.Behavior.NON_BLOCKING;
562578
}
@@ -1023,19 +1039,23 @@ export class RealtimeSession extends llm.RealtimeSession {
10231039
const errorMsg = event.reason || `WebSocket closed with code ${event.code}`;
10241040
this.#logger.error(`Gemini Live session error: ${errorMsg}${truncationNote}`);
10251041

1026-
this.emitError(
1027-
new APIStatusError({
1028-
message: `${errorMsg}${truncationNote}`,
1029-
options: {
1030-
statusCode: event.code,
1031-
retryable: false,
1032-
body: event.reason
1033-
? { reason: event.reason, code: event.code, truncated: isTruncated }
1034-
: null,
1035-
},
1036-
}),
1037-
false,
1038-
);
1042+
const error = new APIStatusError({
1043+
message: `${errorMsg}${truncationNote}`,
1044+
options: {
1045+
statusCode: event.code,
1046+
retryable: false,
1047+
body: event.reason
1048+
? { reason: event.reason, code: event.code, truncated: isTruncated }
1049+
: null,
1050+
},
1051+
});
1052+
1053+
if (event.code === WS_CLOSE_CONTEXT_EXHAUSTED) {
1054+
this.sessionError = error;
1055+
this.markRestartNeeded();
1056+
} else {
1057+
this.emitError(error, false);
1058+
}
10391059
} else {
10401060
this.#logger.debug('Gemini Live session closed:', event.code, event.reason);
10411061
}
@@ -1084,20 +1104,41 @@ export class RealtimeSession extends llm.RealtimeSession {
10841104
}
10851105

10861106
await cancelAndWait([sendTask, restartWaitTask], 2000);
1107+
1108+
if (this.sessionError) {
1109+
const error = this.sessionError;
1110+
this.sessionError = undefined;
1111+
throw error;
1112+
}
10871113
} catch (error) {
1088-
this.#logger.error(`Gemini Realtime API error: ${error}`);
1114+
const err = this.toError(error);
1115+
this.#logger.error(`Gemini Realtime API error: ${err}`);
10891116

10901117
if (this.#closed) break;
10911118

1119+
// Gemini Live closes with 1007 when the session context is exhausted. Reconnecting
1120+
// would replay the same oversized context and fail again, so terminate the session.
1121+
if (this.isContextExhaustedError(err)) {
1122+
this.#logger.error(
1123+
err,
1124+
'Gemini Live closed the session: context exhausted (1007). Reconnecting would replay the same context and fail again; terminating the session.',
1125+
);
1126+
this.emitError(err, false);
1127+
throw new APIConnectionError({
1128+
message: 'Gemini Live session context exhausted (1007)',
1129+
options: { retryable: false },
1130+
});
1131+
}
1132+
10921133
if (maxRetries === 0) {
1093-
this.emitError(error as Error, false);
1134+
this.emitError(err, false);
10941135
throw new APIConnectionError({
10951136
message: 'Failed to connect to Gemini Live',
10961137
});
10971138
}
10981139

10991140
if (this.numRetries >= maxRetries) {
1100-
this.emitError(error as Error, false);
1141+
this.emitError(err, false);
11011142
throw new APIConnectionError({
11021143
message: `Failed to connect to Gemini Live after ${maxRetries} attempts`,
11031144
});
@@ -1190,6 +1231,7 @@ export class RealtimeSession extends llm.RealtimeSession {
11901231
} catch (e) {
11911232
if (!this.sessionShouldClose.isSet) {
11921233
this.#logger.error(`Error in send task: ${e}`);
1234+
this.sessionError = this.toError(e);
11931235
this.markRestartNeeded();
11941236
}
11951237
} finally {
@@ -1303,6 +1345,7 @@ export class RealtimeSession extends llm.RealtimeSession {
13031345
} catch (e) {
13041346
if (!this.sessionShouldClose.isSet) {
13051347
this.#logger.error(`Error in onReceiveMessage: ${e}`);
1348+
this.sessionError = this.toError(e);
13061349
this.markRestartNeeded();
13071350
}
13081351
}

0 commit comments

Comments
 (0)