-
Notifications
You must be signed in to change notification settings - Fork 311
fix(google): surface context exhaustion errors #1841
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@livekit/agents-plugin-google': patch | ||
| --- | ||
|
|
||
| Surface Gemini Live `1007` context exhaustion errors as unrecoverable session errors instead of retrying the same oversized context. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -49,6 +49,7 @@ const LK_GOOGLE_DEBUG = Number(process.env.LK_GOOGLE_DEBUG ?? 0); | |||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // WebSocket close codes (RFC 6455) | ||||||||||||||||||||||||||||||||||||||
| const WS_CLOSE_NORMAL = 1000; | ||||||||||||||||||||||||||||||||||||||
| const WS_CLOSE_CONTEXT_EXHAUSTED = 1007; | ||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚩 WebSocket code 1007 is RFC 6455 'Invalid frame payload data', not a standard context exhaustion code The constant Was this helpful? React with 👍 or 👎 to provide feedback. |
||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||
| * Default image encoding options for Google Realtime API | ||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -473,6 +474,7 @@ export class RealtimeSession extends llm.RealtimeSession { | |||||||||||||||||||||||||||||||||||||
| private inUserActivity = false; | ||||||||||||||||||||||||||||||||||||||
| private sessionLock = new Mutex(); | ||||||||||||||||||||||||||||||||||||||
| private numRetries = 0; | ||||||||||||||||||||||||||||||||||||||
| private sessionError?: Error; | ||||||||||||||||||||||||||||||||||||||
| private hasReceivedAudioInput = false; | ||||||||||||||||||||||||||||||||||||||
| private pendingInterruptText = false; | ||||||||||||||||||||||||||||||||||||||
| private earlyCompletionPending = false; | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -557,6 +559,20 @@ export class RealtimeSession extends llm.RealtimeSession { | |||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| private toError(error: unknown): Error { | ||||||||||||||||||||||||||||||||||||||
| return error instanceof Error ? error : new Error(String(error)); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| private isContextExhaustedError(error: unknown): boolean { | ||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||
| (typeof error === 'object' && | ||||||||||||||||||||||||||||||||||||||
| error !== null && | ||||||||||||||||||||||||||||||||||||||
| 'statusCode' in error && | ||||||||||||||||||||||||||||||||||||||
| error.statusCode === WS_CLOSE_CONTEXT_EXHAUSTED) || | ||||||||||||||||||||||||||||||||||||||
| String(error).includes(String(WS_CLOSE_CONTEXT_EXHAUSTED)) | ||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+566
to
+574
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Overly broad string-based check in The fallback branch How the false positive leads to session termination
The primary
Suggested change
Was this helpful? React with 👍 or 👎 to provide feedback. |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| private isNonBlockingToolBehavior(): boolean { | ||||||||||||||||||||||||||||||||||||||
| return this.options.toolBehavior === types.Behavior.NON_BLOCKING; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -1023,19 +1039,23 @@ export class RealtimeSession extends llm.RealtimeSession { | |||||||||||||||||||||||||||||||||||||
| const errorMsg = event.reason || `WebSocket closed with code ${event.code}`; | ||||||||||||||||||||||||||||||||||||||
| this.#logger.error(`Gemini Live session error: ${errorMsg}${truncationNote}`); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| this.emitError( | ||||||||||||||||||||||||||||||||||||||
| new APIStatusError({ | ||||||||||||||||||||||||||||||||||||||
| message: `${errorMsg}${truncationNote}`, | ||||||||||||||||||||||||||||||||||||||
| options: { | ||||||||||||||||||||||||||||||||||||||
| statusCode: event.code, | ||||||||||||||||||||||||||||||||||||||
| retryable: false, | ||||||||||||||||||||||||||||||||||||||
| body: event.reason | ||||||||||||||||||||||||||||||||||||||
| ? { reason: event.reason, code: event.code, truncated: isTruncated } | ||||||||||||||||||||||||||||||||||||||
| : null, | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||
| false, | ||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||
| const error = new APIStatusError({ | ||||||||||||||||||||||||||||||||||||||
| message: `${errorMsg}${truncationNote}`, | ||||||||||||||||||||||||||||||||||||||
| options: { | ||||||||||||||||||||||||||||||||||||||
| statusCode: event.code, | ||||||||||||||||||||||||||||||||||||||
| retryable: false, | ||||||||||||||||||||||||||||||||||||||
| body: event.reason | ||||||||||||||||||||||||||||||||||||||
| ? { reason: event.reason, code: event.code, truncated: isTruncated } | ||||||||||||||||||||||||||||||||||||||
| : null, | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (event.code === WS_CLOSE_CONTEXT_EXHAUSTED) { | ||||||||||||||||||||||||||||||||||||||
| this.sessionError = error; | ||||||||||||||||||||||||||||||||||||||
| this.markRestartNeeded(); | ||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||
| this.emitError(error, false); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||
| this.#logger.debug('Gemini Live session closed:', event.code, event.reason); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -1084,25 +1104,47 @@ export class RealtimeSession extends llm.RealtimeSession { | |||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| await cancelAndWait([sendTask, restartWaitTask], 2000); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (this.sessionError) { | ||||||||||||||||||||||||||||||||||||||
| const error = this.sessionError; | ||||||||||||||||||||||||||||||||||||||
| this.sessionError = undefined; | ||||||||||||||||||||||||||||||||||||||
| throw error; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||
| this.#logger.error(`Gemini Realtime API error: ${error}`); | ||||||||||||||||||||||||||||||||||||||
| const err = this.toError(error); | ||||||||||||||||||||||||||||||||||||||
| this.#logger.error(`Gemini Realtime API error: ${err}`); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (this.#closed) break; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Gemini Live closes with 1007 when the session context is exhausted. Reconnecting | ||||||||||||||||||||||||||||||||||||||
| // would replay the same oversized context and fail again, so terminate the session. | ||||||||||||||||||||||||||||||||||||||
| if (this.isContextExhaustedError(err)) { | ||||||||||||||||||||||||||||||||||||||
| this.#logger.error( | ||||||||||||||||||||||||||||||||||||||
| err, | ||||||||||||||||||||||||||||||||||||||
| 'Gemini Live closed the session: context exhausted (1007). Reconnecting would replay the same context and fail again; terminating the session.', | ||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||
| this.emitError(err, false); | ||||||||||||||||||||||||||||||||||||||
| throw new APIConnectionError({ | ||||||||||||||||||||||||||||||||||||||
| message: 'Gemini Live session context exhausted (1007)', | ||||||||||||||||||||||||||||||||||||||
| options: { retryable: false }, | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (maxRetries === 0) { | ||||||||||||||||||||||||||||||||||||||
| this.emitError(error as Error, false); | ||||||||||||||||||||||||||||||||||||||
| this.emitError(err, false); | ||||||||||||||||||||||||||||||||||||||
| throw new APIConnectionError({ | ||||||||||||||||||||||||||||||||||||||
| message: 'Failed to connect to Gemini Live', | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (this.numRetries >= maxRetries) { | ||||||||||||||||||||||||||||||||||||||
| this.emitError(error as Error, false); | ||||||||||||||||||||||||||||||||||||||
| this.emitError(err, false); | ||||||||||||||||||||||||||||||||||||||
| throw new APIConnectionError({ | ||||||||||||||||||||||||||||||||||||||
| message: `Failed to connect to Gemini Live after ${maxRetries} attempts`, | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| this.emitError(err, true); | ||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚩 New Line 1147 adds Was this helpful? React with 👍 or 👎 to provide feedback. |
||||||||||||||||||||||||||||||||||||||
| const retryInterval = | ||||||||||||||||||||||||||||||||||||||
| this.numRetries === 100 ? 0 : this.options.connOptions.retryIntervalMs; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
@@ -1190,6 +1232,7 @@ export class RealtimeSession extends llm.RealtimeSession { | |||||||||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||||||||||
| if (!this.sessionShouldClose.isSet) { | ||||||||||||||||||||||||||||||||||||||
| this.#logger.error(`Error in send task: ${e}`); | ||||||||||||||||||||||||||||||||||||||
| this.sessionError = this.toError(e); | ||||||||||||||||||||||||||||||||||||||
| this.markRestartNeeded(); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1235
to
1237
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚩 Behavioral change: sendTask/onReceiveMessage errors now count against retry budget Prior to this PR, errors in Was this helpful? React with 👍 or 👎 to provide feedback. |
||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -1303,6 +1346,7 @@ export class RealtimeSession extends llm.RealtimeSession { | |||||||||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||||||||||
| if (!this.sessionShouldClose.isSet) { | ||||||||||||||||||||||||||||||||||||||
| this.#logger.error(`Error in onReceiveMessage: ${e}`); | ||||||||||||||||||||||||||||||||||||||
| this.sessionError = this.toError(e); | ||||||||||||||||||||||||||||||||||||||
| this.markRestartNeeded(); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Stale
sessionErrornot cleared between retry iterations can terminate a healthy sessionThe new
sessionErrorfield is set byonclose,sendTask, andonReceiveMessagecallbacks, and is intended to be consumed (and cleared) atplugins/google/src/realtime/realtime_api.ts:1097-1101. However, ifcancelAndWaitat line 1095 throws (e.g., due to a 2-second timeout), execution jumps directly to thecatchblock, skipping thesessionErrorcheck. The stalesessionErroris never cleared — neither bycloseActiveSession()(plugins/google/src/realtime/realtime_api.ts:523-544) nor at the top of the nextwhileiteration (line 993-997). On the next iteration, after a potentially successful new session, the stalesessionErroris discovered at line 1097, thrown, and processed. If the stale error was a 1007 context-exhaustion error,isContextExhaustedErrorwould return true at line 1110, causing the perfectly healthy new session to be terminated.Was this helpful? React with 👍 or 👎 to provide feedback.