Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class _BidiPageState extends State<BidiPage> {
super.initState();

final config = LiveGenerationConfig(
speechConfig: SpeechConfig(voice: Voice.Fenrir),
speechConfig: SpeechConfig(voice: Voice.fenrir),
responseModalities: [
ResponseModalities.audio,
],
Expand Down
13 changes: 11 additions & 2 deletions packages/firebase_vertexai/firebase_vertexai/lib/src/error.dart
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,15 @@ final class LiveWebSocketClosedException implements Exception {
final String message;

@override
String toString() => message;
String toString() {
if (message.contains('DEADLINE_EXCEEDED')) {
return 'The current live session has expired. Please start a new session.';
} else if (message.contains('RESOURCE_EXHAUSTED')) {
return 'You have exceeded the maximum number of concurrent sessions. '
'Please close other sessions and try again later.';
}
return message;
}
}

/// Parse the error json object.
Expand All @@ -150,7 +158,8 @@ VertexAIException parseError(Object jsonObject) {
} =>
InvalidApiKey(message),
{'message': UnsupportedUserLocation._message} => UnsupportedUserLocation(),
{'message': final String message} when message.contains('quota') =>
{'message': final String message}
when message.toLowerCase().contains('quota') =>
QuotaExceeded(message),
{
'message': final String _,
Expand Down
20 changes: 10 additions & 10 deletions packages/firebase_vertexai/firebase_vertexai/lib/src/live_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,20 @@ import 'error.dart';

/// The available voice options for speech synthesis.
enum Voice {
// ignore: public_member_api_docs, constant_identifier_names
Aoede('Aoede'),
// ignore: public_member_api_docs
aoede('Aoede'),

// ignore: public_member_api_docs, constant_identifier_names
Charon('Charon'),
// ignore: public_member_api_docs
charon('Charon'),

// ignore: public_member_api_docs, constant_identifier_names
Fenrir('Fenrir'),
// ignore: public_member_api_docs
fenrir('Fenrir'),

// ignore: public_member_api_docs, constant_identifier_names
Kore('Kore'),
// ignore: public_member_api_docs
kore('Kore'),

// ignore: public_member_api_docs, constant_identifier_names
Puck('Puck');
// ignore: public_member_api_docs
puck('Puck');

const Voice(this._jsonString);
final String _jsonString;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ class LiveSession {
void _checkWsStatus() {
if (_ws.closeCode != null) {
var message =
'WebSocket status: Closed, closeCode: ${_ws.closeCode}, closeReason: ${_ws.closeReason}';
'WebSocket Closed, closeCode: ${_ws.closeCode}, closeReason: ${_ws.closeReason}';

throw LiveWebSocketClosedException(message);
}
Expand Down
165 changes: 165 additions & 0 deletions packages/firebase_vertexai/firebase_vertexai/test/error_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import 'package:firebase_vertexai/src/error.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
group('VertexAI Exceptions', () {
test('VertexAIException toString', () {
final exception = VertexAIException('Test message');
expect(exception.toString(), 'VertexAIException: Test message');
});

test('InvalidApiKey toString', () {
final exception = InvalidApiKey('Invalid API key provided.');
expect(exception.toString(), 'Invalid API key provided.');
});

test('UnsupportedUserLocation message', () {
final exception = UnsupportedUserLocation();
expect(
exception.message, 'User location is not supported for the API use.');
});

test('ServiceApiNotEnabled message', () {
final exception = ServiceApiNotEnabled('projects/test-project');
expect(
exception.message,
'The Vertex AI in Firebase SDK requires the Vertex AI in Firebase API '
'(`firebasevertexai.googleapis.com`) to be enabled in your Firebase project. Enable this API '
'by visiting the Firebase Console at '
'https://console.firebase.google.com/project/test-project/genai '
'and clicking "Get started". If you enabled this API recently, wait a few minutes for the '
'action to propagate to our systems and then retry.');
});

test('QuotaExceeded toString', () {
final exception = QuotaExceeded('Quota for this API has been exceeded.');
expect(exception.toString(), 'Quota for this API has been exceeded.');
});

test('ServerException toString', () {
final exception = ServerException('Server error occurred.');
expect(exception.toString(), 'Server error occurred.');
});

test('VertexAISdkException toString', () {
final exception = VertexAISdkException('SDK failed to parse response.');
expect(
exception.toString(),
'SDK failed to parse response.\n'
'This indicates a problem with the Vertex AI in Firebase SDK. '
'Try updating to the latest version '
'(https://pub.dev/packages/firebase_vertexai/versions), '
'or file an issue at '
'https://github.com/firebase/flutterfire/issues.');
});

test('ImagenImagesBlockedException toString', () {
final exception =
ImagenImagesBlockedException('All images were blocked.');
expect(exception.toString(), 'All images were blocked.');
});

test('LiveWebSocketClosedException toString - DEADLINE_EXCEEDED', () {
final exception = LiveWebSocketClosedException(
'DEADLINE_EXCEEDED: Connection timed out.');
expect(exception.toString(),
'The current live session has expired. Please start a new session.');
});

test('LiveWebSocketClosedException toString - RESOURCE_EXHAUSTED', () {
final exception = LiveWebSocketClosedException(
'RESOURCE_EXHAUSTED: Too many connections.');
expect(
exception.toString(),
'You have exceeded the maximum number of concurrent sessions. '
'Please close other sessions and try again later.');
});

test('LiveWebSocketClosedException toString - Other', () {
final exception =
LiveWebSocketClosedException('WebSocket connection closed.');
expect(exception.toString(), 'WebSocket connection closed.');
});

group('parseError', () {
test('parses API_KEY_INVALID', () {
final json = {
'message': 'Invalid API key',
'details': [
{'reason': 'API_KEY_INVALID'}
]
};
final exception = parseError(json);
expect(exception, isInstanceOf<InvalidApiKey>());
expect(exception.message, 'Invalid API key');
});

test('parses UNSUPPORTED_USER_LOCATION', () {
final json = {
'message': 'User location is not supported for the API use.'
};
final exception = parseError(json);
expect(exception, isInstanceOf<UnsupportedUserLocation>());
});

test('parses QUOTA_EXCEEDED', () {
final json = {'message': 'Quota exceeded: Limit reached.'};
final exception = parseError(json);
expect(exception, isInstanceOf<QuotaExceeded>());
expect(exception.message, 'Quota exceeded: Limit reached.');
});

test('parses SERVICE_API_NOT_ENABLED', () {
final json = {
'message': 'API not enabled',
'status': 'PERMISSION_DENIED',
'details': [
{
'metadata': {
'service': 'firebasevertexai.googleapis.com',
'consumer': 'projects/my-project-id',
}
}
]
};
final exception = parseError(json);
expect(exception, isInstanceOf<ServiceApiNotEnabled>());
expect(
(exception as ServiceApiNotEnabled).message,
'The Vertex AI in Firebase SDK requires the Vertex AI in Firebase API '
'(`firebasevertexai.googleapis.com`) to be enabled in your Firebase project. Enable this API '
'by visiting the Firebase Console at '
'https://console.firebase.google.com/project/my-project-id/genai '
'and clicking "Get started". If you enabled this API recently, wait a few minutes for the '
'action to propagate to our systems and then retry.');
});

test('parses SERVER_ERROR', () {
final json = {'message': 'Internal server error.'};
final exception = parseError(json);
expect(exception, isInstanceOf<ServerException>());
expect(exception.message, 'Internal server error.');
});

test('parses UNHANDLED_FORMAT', () {
final json = {'unexpected': 'format'};
expect(() => parseError(json),
throwsA(isInstanceOf<VertexAISdkException>()));
});
});
});
}
14 changes: 7 additions & 7 deletions packages/firebase_vertexai/firebase_vertexai/test/live_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ import 'package:flutter_test/flutter_test.dart';
void main() {
group('LiveAPI Tests', () {
test('Voices enum toJson() returns correct value', () {
expect(Voice.Aoede.toJson(), 'Aoede');
expect(Voice.Charon.toJson(), 'Charon');
expect(Voice.Fenrir.toJson(), 'Fenrir');
expect(Voice.Kore.toJson(), 'Kore');
expect(Voice.Puck.toJson(), 'Puck');
expect(Voice.aoede.toJson(), 'Aoede');
expect(Voice.charon.toJson(), 'Charon');
expect(Voice.fenrir.toJson(), 'Fenrir');
expect(Voice.kore.toJson(), 'Kore');
expect(Voice.puck.toJson(), 'Puck');
});

test('SpeechConfig toJson() returns correct JSON', () {
final speechConfigWithVoice = SpeechConfig(voice: Voice.Aoede);
final speechConfigWithVoice = SpeechConfig(voice: Voice.aoede);
expect(speechConfigWithVoice.toJson(), {
'voice_config': {
'prebuilt_voice_config': {'voice_name': 'Aoede'}
Expand All @@ -49,7 +49,7 @@ void main() {

test('LiveGenerationConfig toJson() returns correct JSON', () {
final liveGenerationConfig = LiveGenerationConfig(
speechConfig: SpeechConfig(voice: Voice.Charon),
speechConfig: SpeechConfig(voice: Voice.charon),
responseModalities: [ResponseModalities.text, ResponseModalities.audio],
candidateCount: 2,
maxOutputTokens: 100,
Expand Down
Loading