Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f5081e1
add structure for live session resumption
cynthiajoan Dec 12, 2025
f4b7b47
session resumption config should be session based
cynthiajoan Dec 12, 2025
0dc39e5
init setup
cynthiajoan Jan 5, 2026
cde74b7
Merge branch 'main' into firebaseai/live_session_resumption
cynthiajoan Jan 29, 2026
7f52d8a
a bit session management fix
cynthiajoan Jan 29, 2026
a944f56
some update for session management
cynthiajoan Feb 4, 2026
cf4f29d
refactor live_session connect and resume api
cynthiajoan Feb 5, 2026
dadb0f8
refactor for websocket session open
cynthiajoan Feb 5, 2026
eb708fc
Merge branch 'main' into firebaseai/live_session_resumption
cynthiajoan Feb 24, 2026
81eb5ac
fix some error while handling the received message
cynthiajoan Feb 25, 2026
ac5568a
some clean up, and more dev logs
cynthiajoan Feb 26, 2026
abe62db
session resume with toggle
cynthiajoan Feb 27, 2026
fc44d2a
Merge branch 'main' into firebaseai/live_session_resumption
cynthiajoan Mar 4, 2026
9e6b95d
some minor updates
cynthiajoan Mar 5, 2026
af619cf
Merge branch 'main' into firebaseai/live_session_resumption
cynthiajoan Apr 13, 2026
b080c25
fix analyzer
cynthiajoan Apr 13, 2026
fb5b029
somehow worked resume session
cynthiajoan Apr 15, 2026
9ac10aa
add google search system tool for bidi page
cynthiajoan Apr 16, 2026
50daaee
some clean up
cynthiajoan Apr 17, 2026
4b7d1dd
format
cynthiajoan Apr 21, 2026
c849e71
remove unnecessary logs
cynthiajoan Apr 21, 2026
048b80e
minor tweak
cynthiajoan Apr 21, 2026
dec7227
fix analyzer
cynthiajoan Apr 21, 2026
927ac9c
review feedback
cynthiajoan Apr 22, 2026
9529ebe
fix format
cynthiajoan Apr 22, 2026
a8bfaea
Merge branch 'main' into firebaseai/live_session_resumption
cynthiajoan Apr 22, 2026
d11d19f
add the sliding window documentation
cynthiajoan Apr 22, 2026
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
1,058 changes: 608 additions & 450 deletions packages/firebase_ai/firebase_ai/example/lib/pages/bidi_page.dart

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ class _ChatPageState extends State<ChatPage> {
Icons.send,
color: Theme.of(context).colorScheme.primary,
),
tooltip: 'Send',
),
IconButton(
onPressed: () {
Expand All @@ -162,6 +163,7 @@ class _ChatPageState extends State<ChatPage> {
Icons.stream,
color: Theme.of(context).colorScheme.primary,
),
tooltip: 'Send Stream',
),
],
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,6 @@ class AudioInput extends ChangeNotifier {
sampleRate: 24000,
device: selectedDevice,
numChannels: 1,
echoCancel: true,
noiseSuppress: true,
androidConfig: const AndroidRecordConfig(
audioSource: AndroidAudioSource.voiceCommunication,
),
Expand Down
10 changes: 7 additions & 3 deletions packages/firebase_ai/firebase_ai/lib/firebase_ai.dart
Original file line number Diff line number Diff line change
Expand Up @@ -102,15 +102,19 @@ export 'src/imagen/imagen_reference.dart'
ImagenControlReference;
export 'src/live_api.dart'
show
LiveGenerationConfig,
SpeechConfig,
AudioTranscriptionConfig,
ContextWindowCompressionConfig,
GoingAwayNotice,
LiveGenerationConfig,
LiveServerMessage,
LiveServerContent,
LiveServerToolCall,
LiveServerToolCallCancellation,
LiveServerResponse,
GoingAwayNotice,
SessionResumptionConfig,
SessionResumptionUpdate,
SlidingWindow,
SpeechConfig,
Transcription;
export 'src/live_session.dart' show LiveSession;
export 'src/schema.dart' show JSONSchema, Schema, SchemaType;
Expand Down
4 changes: 0 additions & 4 deletions packages/firebase_ai/firebase_ai/lib/src/base_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,12 @@
// limitations under the License.

import 'dart:async';
import 'dart:convert';

import 'package:firebase_app_check/firebase_app_check.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:meta/meta.dart';
import 'package:web_socket_channel/io.dart';
import 'package:web_socket_channel/web_socket_channel.dart';

import 'api.dart';
import 'client.dart';
Expand Down
138 changes: 125 additions & 13 deletions packages/firebase_ai/firebase_ai/lib/src/live_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -77,21 +77,93 @@ class AudioTranscriptionConfig {
Map<String, Object?> toJson() => {};
}

/// Configures the sliding window context compression mechanism.
///
/// The SlidingWindow method operates by discarding content at the beginning of
/// the context window. The resulting context will always begin at the start of
/// a USER role turn. System instructions will always remain at the start of the
/// result.
class SlidingWindow {
/// Creates a [SlidingWindow] instance.
///
/// [targetTokens] (optional): The target number of tokens to keep in the
/// context window.
SlidingWindow({this.targetTokens});

/// The session reduction target, i.e., how many tokens we should keep.
final int? targetTokens;
// ignore: public_member_api_docs
Map<String, Object?> toJson() =>
{if (targetTokens case final targetTokens?) 'targetTokens': targetTokens};
}

/// Enables context window compression to manage the model's context window.
///
/// This mechanism prevents the context from exceeding a given length.
class ContextWindowCompressionConfig {
/// Creates a [ContextWindowCompressionConfig] instance.
///
/// [triggerTokens] (optional): The number of tokens that triggers the
/// compression mechanism.
/// [slidingWindow] (optional): The sliding window compression mechanism to
/// use.
ContextWindowCompressionConfig({this.triggerTokens, this.slidingWindow});

/// The number of tokens (before running a turn) that triggers the context
/// window compression.
final int? triggerTokens;

/// The sliding window compression mechanism.
final SlidingWindow? slidingWindow;
// ignore: public_member_api_docs
Map<String, Object?> toJson() => {
if (triggerTokens case final triggerTokens?)
'triggerTokens': triggerTokens,
if (slidingWindow case final slidingWindow?)
'slidingWindow': slidingWindow.toJson()
};
}

/// Configuration for the session resumption mechanism.
///
/// When included in the session setup, the server will send
/// [SessionResumptionUpdate] messages.
class SessionResumptionConfig {
/// Creates a [SessionResumptionConfig] instance.
///
/// [handle] (optional): The session resumption handle of the previous session
/// to restore.
/// [transparent] (optional): If set, requests the server to send updates with
/// the message index of the last client message included in the session
/// state.
SessionResumptionConfig({this.handle});

/// The session resumption handle of the previous session to restore.
///
/// If not present, a new session will be started.
final String? handle;

// ignore: public_member_api_docs
Map<String, Object?> toJson() => {
if (handle case final handle?) 'handle': handle,
};
}

/// Configures live generation settings.
final class LiveGenerationConfig extends BaseGenerationConfig {
// ignore: public_member_api_docs
LiveGenerationConfig({
this.speechConfig,
this.inputAudioTranscription,
this.outputAudioTranscription,
super.responseModalities,
super.maxOutputTokens,
super.temperature,
super.topP,
super.topK,
super.presencePenalty,
super.frequencyPenalty,
});
LiveGenerationConfig(
{this.speechConfig,
this.inputAudioTranscription,
this.outputAudioTranscription,
this.contextWindowCompression,
super.responseModalities,
super.maxOutputTokens,
super.temperature,
super.topP,
super.topK,
super.presencePenalty,
super.frequencyPenalty});

/// The speech configuration.
final SpeechConfig? speechConfig;
Expand All @@ -103,6 +175,9 @@ final class LiveGenerationConfig extends BaseGenerationConfig {
/// the output audio.
final AudioTranscriptionConfig? outputAudioTranscription;

/// The context window compression configuration.
final ContextWindowCompressionConfig? contextWindowCompression;

@override
Map<String, Object?> toJson() => {
...super.toJson(),
Expand Down Expand Up @@ -222,6 +297,34 @@ class GoingAwayNotice implements LiveServerMessage {
final String? timeLeft;
}

/// An update of the session resumption state.
///
/// This message is only sent if [SessionResumptionConfig] was set in the
/// session setup.
class SessionResumptionUpdate implements LiveServerMessage {
/// Creates a [SessionResumptionUpdate] instance.
///
/// [newHandle] (optional): The new handle that represents the state that can
/// be resumed.
/// [resumable] (optional): Indicates if the session can be resumed at this
/// point.
/// [lastConsumedClientMessageIndex] (optional): The index of the last client
/// message that is included in the state represented by this update.
SessionResumptionUpdate(
{this.newHandle, this.resumable, this.lastConsumedClientMessageIndex});

/// The new handle that represents the state that can be resumed. Empty if
/// `resumable` is false.
final String? newHandle;

/// Indicates if the session can be resumed at this point.
final bool? resumable;

/// The index of the last client message that is included in the state
/// represented by this update.
final int? lastConsumedClientMessageIndex;
}

/// A single response chunk received during a live content generation.
///
/// It can contain generated content, function calls to be executed, or
Expand Down Expand Up @@ -449,8 +552,17 @@ LiveServerMessage _parseServerMessage(Object jsonObject) {
} else if (json.containsKey('setupComplete')) {
return LiveServerSetupComplete();
} else if (json.containsKey('goAway')) {
final goAwayJson = json['goAway'] as Map;
final goAwayJson = json['goAway'] as Map<String, dynamic>;
return GoingAwayNotice(timeLeft: goAwayJson['timeLeft'] as String?);
} else if (json.containsKey('sessionResumptionUpdate')) {
final sessionResumptionUpdateJson =
json['sessionResumptionUpdate'] as Map<String, dynamic>;
return SessionResumptionUpdate(
newHandle: sessionResumptionUpdateJson['newHandle'] as String?,
resumable: sessionResumptionUpdateJson['resumable'] as bool?,
lastConsumedClientMessageIndex:
sessionResumptionUpdateJson['lastConsumedClientMessageIndex'] as int?,
);
} else {
throw unhandledFormat('LiveServerMessage', json);
}
Expand Down
41 changes: 13 additions & 28 deletions packages/firebase_ai/firebase_ai/lib/src/live_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -90,48 +90,33 @@ final class LiveGenerativeModel extends BaseModel {
///
/// This function handles the WebSocket connection setup and returns an [LiveSession]
/// object that can be used to communicate with the service.
/// [sessionResumption] (optional): The configuration for session resumption,
/// such as the handle to the previous session state to restore.
///
/// Returns a [Future] that resolves to an [LiveSession] object upon successful
/// connection.
Future<LiveSession> connect() async {
Future<LiveSession> connect(
{SessionResumptionConfig? sessionResumption}) async {
Comment thread
cynthiajoan marked this conversation as resolved.
final uri = _useVertexBackend ? _vertexAIUri() : _googleAIUri();
final modelString =
_useVertexBackend ? _vertexAIModelString() : _googleAIModelString();

final setupJson = {
'setup': {
'model': modelString,
if (_systemInstruction != null)
'system_instruction': _systemInstruction.toJson(),
if (_tools != null) 'tools': _tools.map((t) => t.toJson()).toList(),
if (_liveGenerationConfig != null) ...{
'generation_config': _liveGenerationConfig.toJson(),
if (_liveGenerationConfig.inputAudioTranscription != null)
'input_audio_transcription':
_liveGenerationConfig.inputAudioTranscription!.toJson(),
if (_liveGenerationConfig.outputAudioTranscription != null)
'output_audio_transcription':
_liveGenerationConfig.outputAudioTranscription!.toJson(),
},
}
};

final request = jsonEncode(setupJson);
final headers = await BaseModel.firebaseTokens(
_appCheck,
_auth,
_app,
_useLimitedUseAppCheckTokens,
)();

var ws = kIsWeb
? WebSocketChannel.connect(Uri.parse(uri))
: IOWebSocketChannel.connect(Uri.parse(uri), headers: headers);
await ws.ready;

ws.sink.add(request);

return LiveSession(ws);
return LiveSession.create(
uri: uri,
headers: headers,
modelString: modelString,
systemInstruction: _systemInstruction,
tools: _tools,
sessionResumption: sessionResumption,
liveGenerationConfig: _liveGenerationConfig,
);
}
}

Expand Down
Loading
Loading