



























@@ -1,20 +1,20 @@
11import { randomUUID } from "node:crypto";
2-import {
2+import type {
33ActivityHandling,
44Behavior,
55EndSensitivity,
6+FunctionDeclaration,
7+FunctionResponse,
68FunctionResponseScheduling,
9+LiveConnectConfig,
10+LiveServerContent,
11+LiveServerMessage,
12+LiveServerToolCall,
713Modality,
14+RealtimeInputConfig,
815StartSensitivity,
16+ThinkingConfig,
917TurnCoverage,
10-type FunctionDeclaration,
11-type FunctionResponse,
12-type LiveConnectConfig,
13-type LiveServerContent,
14-type LiveServerMessage,
15-type LiveServerToolCall,
16-type RealtimeInputConfig,
17-type ThinkingConfig,
1818} from "@google/genai";
1919import type { OpenClawConfig } from "openclaw/plugin-sdk/provider-onboard";
2020import type {
@@ -47,7 +47,7 @@ const GOOGLE_REALTIME_BROWSER_API_VERSION = "v1alpha";
4747const GOOGLE_REALTIME_BROWSER_WEBSOCKET_URL =
4848"wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1alpha.GenerativeService.BidiGenerateContentConstrained";
4949const MAX_PENDING_AUDIO_CHUNKS = 320;
50-const DEFAULT_AUDIO_STREAM_END_SILENCE_MS = 700;
50+const DEFAULT_AUDIO_STREAM_END_SILENCE_MS = 500;
5151const GOOGLE_REALTIME_BROWSER_SESSION_TTL_MS = 30 * 60 * 1000;
5252const GOOGLE_REALTIME_BROWSER_NEW_SESSION_TTL_MS = 60 * 1000;
5353@@ -70,6 +70,8 @@ type GoogleRealtimeVoiceProviderConfig = {
7070turnCoverage?: GoogleRealtimeTurnCoverage;
7171automaticActivityDetectionDisabled?: boolean;
7272enableAffectiveDialog?: boolean;
73+sessionResumption?: boolean;
74+contextWindowCompression?: boolean;
7375thinkingLevel?: GoogleRealtimeThinkingLevel;
7476thinkingBudget?: number;
7577};
@@ -90,6 +92,8 @@ type GoogleRealtimeLiveConfig = {
9092turnCoverage?: GoogleRealtimeTurnCoverage;
9193automaticActivityDetectionDisabled?: boolean;
9294enableAffectiveDialog?: boolean;
95+sessionResumption?: boolean;
96+contextWindowCompression?: boolean;
9397thinkingLevel?: GoogleRealtimeThinkingLevel;
9498thinkingBudget?: number;
9599};
@@ -209,6 +213,8 @@ function normalizeProviderConfig(
209213turnCoverage: asTurnCoverage(raw?.turnCoverage),
210214automaticActivityDetectionDisabled: asBoolean(raw?.automaticActivityDetectionDisabled),
211215enableAffectiveDialog: asBoolean(raw?.enableAffectiveDialog),
216+sessionResumption: asBoolean(raw?.sessionResumption),
217+contextWindowCompression: asBoolean(raw?.contextWindowCompression),
212218thinkingLevel: asThinkingLevel(raw?.thinkingLevel),
213219thinkingBudget: asFiniteNumber(raw?.thinkingBudget),
214220};
@@ -223,9 +229,9 @@ function mapStartSensitivity(
223229): StartSensitivity | undefined {
224230switch (value) {
225231case "high":
226-return StartSensitivity.START_SENSITIVITY_HIGH;
232+return "START_SENSITIVITY_HIGH" as StartSensitivity;
227233case "low":
228-return StartSensitivity.START_SENSITIVITY_LOW;
234+return "START_SENSITIVITY_LOW" as StartSensitivity;
229235default:
230236return undefined;
231237}
@@ -236,9 +242,9 @@ function mapEndSensitivity(
236242): EndSensitivity | undefined {
237243switch (value) {
238244case "high":
239-return EndSensitivity.END_SENSITIVITY_HIGH;
245+return "END_SENSITIVITY_HIGH" as EndSensitivity;
240246case "low":
241-return EndSensitivity.END_SENSITIVITY_LOW;
247+return "END_SENSITIVITY_LOW" as EndSensitivity;
242248default:
243249return undefined;
244250}
@@ -249,9 +255,9 @@ function mapActivityHandling(
249255): ActivityHandling | undefined {
250256switch (value) {
251257case "no-interruption":
252-return ActivityHandling.NO_INTERRUPTION;
258+return "NO_INTERRUPTION" as ActivityHandling;
253259case "start-of-activity-interrupts":
254-return ActivityHandling.START_OF_ACTIVITY_INTERRUPTS;
260+return "START_OF_ACTIVITY_INTERRUPTS" as ActivityHandling;
255261default:
256262return undefined;
257263}
@@ -260,11 +266,11 @@ function mapActivityHandling(
260266function mapTurnCoverage(value: GoogleRealtimeTurnCoverage | undefined): TurnCoverage | undefined {
261267switch (value) {
262268case "only-activity":
263-return TurnCoverage.TURN_INCLUDES_ONLY_ACTIVITY;
269+return "TURN_INCLUDES_ONLY_ACTIVITY" as TurnCoverage;
264270case "all-input":
265-return TurnCoverage.TURN_INCLUDES_ALL_INPUT;
271+return "TURN_INCLUDES_ALL_INPUT" as TurnCoverage;
266272case "audio-activity-and-all-video":
267-return TurnCoverage.TURN_INCLUDES_AUDIO_ACTIVITY_AND_ALL_VIDEO;
273+return "TURN_INCLUDES_AUDIO_ACTIVITY_AND_ALL_VIDEO" as TurnCoverage;
268274default:
269275return undefined;
270276}
@@ -316,7 +322,7 @@ function buildFunctionDeclarations(tools: RealtimeVoiceTool[] | undefined): Func
316322parametersJsonSchema: tool.parameters,
317323};
318324if (tool.name === REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME) {
319-declaration.behavior = Behavior.NON_BLOCKING;
325+declaration.behavior = "NON_BLOCKING" as Behavior;
320326}
321327return declaration;
322328});
@@ -325,7 +331,7 @@ function buildFunctionDeclarations(tools: RealtimeVoiceTool[] | undefined): Func
325331function buildGoogleLiveConnectConfig(config: GoogleRealtimeLiveConfig): LiveConnectConfig {
326332const functionDeclarations = buildFunctionDeclarations(config.tools);
327333return {
328-responseModalities: [Modality.AUDIO],
334+responseModalities: ["AUDIO" as Modality],
329335 ...(typeof config.temperature === "number" && config.temperature > 0
330336 ? { temperature: config.temperature }
331337 : {}),
@@ -359,7 +365,7 @@ function buildBrowserInitialSetup(model: string) {
359365setup: {
360366model: toGoogleModelResource(model),
361367generationConfig: {
362-responseModalities: [Modality.AUDIO],
368+responseModalities: ["AUDIO" as Modality],
363369},
364370inputAudioTranscription: {},
365371outputAudioTranscription: {},
@@ -403,6 +409,7 @@ class GoogleRealtimeVoiceBridge implements RealtimeVoiceBridge {
403409private audioStreamEnded = false;
404410private pendingFunctionNames = new Map<string, string>();
405411private readonly audioFormat: RealtimeVoiceAudioFormat;
412+private resumptionHandle: string | undefined;
406413407414constructor(private readonly config: GoogleRealtimeVoiceBridgeConfig) {
408415this.audioFormat = config.audioFormat ?? REALTIME_VOICE_AUDIO_FORMAT_G711_ULAW_8KHZ;
@@ -425,7 +432,17 @@ class GoogleRealtimeVoiceBridge implements RealtimeVoiceBridge {
425432426433this.session = (await ai.live.connect({
427434model: this.config.model ?? GOOGLE_REALTIME_DEFAULT_MODEL,
428-config: buildGoogleLiveConnectConfig(this.config),
435+config: {
436+ ...buildGoogleLiveConnectConfig(this.config),
437+ ...(this.config.sessionResumption === false
438+ ? {}
439+ : {
440+sessionResumption: this.resumptionHandle ? { handle: this.resumptionHandle } : {},
441+}),
442+ ...(this.config.contextWindowCompression === false
443+ ? {}
444+ : { contextWindowCompression: { slidingWindow: {} } }),
445+},
429446callbacks: {
430447onopen: () => {
431448this.connected = true;
@@ -548,7 +565,7 @@ class GoogleRealtimeVoiceBridge implements RealtimeVoiceBridge {
548565 : { output: result },
549566};
550567if (isConsultTool) {
551-functionResponse.scheduling = FunctionResponseScheduling.WHEN_IDLE;
568+functionResponse.scheduling = "WHEN_IDLE" as FunctionResponseScheduling;
552569if (options?.willContinue === true) {
553570functionResponse.willContinue = true;
554571}
@@ -607,6 +624,7 @@ class GoogleRealtimeVoiceBridge implements RealtimeVoiceBridge {
607624}
608625609626private handleMessage(message: LiveServerMessage): void {
627+this.captureSessionLifecycle(message);
610628if (message.setupComplete) {
611629this.handleSetupComplete();
612630}
@@ -618,6 +636,20 @@ class GoogleRealtimeVoiceBridge implements RealtimeVoiceBridge {
618636}
619637}
620638639+private captureSessionLifecycle(message: LiveServerMessage): void {
640+const raw = message as unknown as {
641+goAway?: { timeLeft?: string };
642+sessionResumptionUpdate?: { newHandle?: string; resumable?: boolean };
643+};
644+const update = raw.sessionResumptionUpdate;
645+if (update?.resumable && update.newHandle) {
646+this.resumptionHandle = update.newHandle;
647+}
648+if (raw.goAway?.timeLeft) {
649+this.config.onError?.(new Error(`Google Live session goAway: ${raw.goAway.timeLeft}`));
650+}
651+}
652+621653private handleSetupComplete(): void {
622654this.sessionConfigured = true;
623655for (const chunk of this.pendingAudio.splice(0)) {
@@ -784,6 +816,8 @@ export function buildGoogleRealtimeVoiceProvider(): RealtimeVoiceProviderPlugin
784816turnCoverage: config.turnCoverage,
785817automaticActivityDetectionDisabled: config.automaticActivityDetectionDisabled,
786818enableAffectiveDialog: config.enableAffectiveDialog,
819+sessionResumption: config.sessionResumption,
820+contextWindowCompression: config.contextWindowCompression,
787821thinkingLevel: config.thinkingLevel,
788822thinkingBudget: config.thinkingBudget,
789823});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。