Skip to content
5 changes: 3 additions & 2 deletions packages/firebase_ai/firebase_ai/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// limitations under the License.

import 'package:firebase_ai/firebase_ai.dart';

import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
Expand Down Expand Up @@ -70,10 +71,10 @@ class _GenerativeAISampleState extends State<GenerativeAISample> {

void _initializeModel(bool useVertexBackend) {
if (useVertexBackend) {
final vertexInstance = FirebaseAI.vertexAI(auth: FirebaseAuth.instance);
final vertexInstance = FirebaseAI.vertexAI();
_currentModel = vertexInstance.generativeModel(model: 'gemini-2.5-flash');
} else {
final googleAI = FirebaseAI.googleAI(auth: FirebaseAuth.instance);
final googleAI = FirebaseAI.googleAI();
_currentModel = googleAI.generativeModel(model: 'gemini-2.5-flash');
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:firebase_ai/firebase_ai.dart';
import '../widgets/message_widget.dart';
Expand Down Expand Up @@ -57,12 +56,12 @@ class _ChatPageState extends State<ChatPage> {
: null,
);
if (widget.useVertexBackend) {
_model = FirebaseAI.vertexAI(auth: FirebaseAuth.instance).generativeModel(
_model = FirebaseAI.vertexAI().generativeModel(
model: 'gemini-2.5-flash',
generationConfig: generationConfig,
);
} else {
_model = FirebaseAI.googleAI(auth: FirebaseAuth.instance).generativeModel(
_model = FirebaseAI.googleAI().generativeModel(
model: 'gemini-2.5-flash',
generationConfig: generationConfig,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import 'package:flutter/material.dart';
import 'package:firebase_ai/firebase_ai.dart';
import 'package:firebase_auth/firebase_auth.dart';

import '../utils/function_call_utils.dart';
import '../widgets/message_widget.dart';

Expand Down Expand Up @@ -235,9 +235,8 @@ class _FunctionCallingPageState extends State<FunctionCallingPage> {
: null,
);

final aiClient = widget.useVertexBackend
? FirebaseAI.vertexAI(auth: FirebaseAuth.instance)
: FirebaseAI.googleAI(auth: FirebaseAuth.instance);
final aiClient =
widget.useVertexBackend ? FirebaseAI.vertexAI() : FirebaseAI.googleAI();

_functionCallModel = aiClient.generativeModel(
model: 'gemini-2.5-flash',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:firebase_ai/firebase_ai.dart';
import '../widgets/message_widget.dart';
Expand Down Expand Up @@ -74,9 +73,8 @@ class _GroundingPageState extends State<GroundingPage> {
}
}

final aiProvider = widget.useVertexBackend
? FirebaseAI.vertexAI(auth: FirebaseAuth.instance)
: FirebaseAI.googleAI(auth: FirebaseAuth.instance);
final aiProvider =
widget.useVertexBackend ? FirebaseAI.vertexAI() : FirebaseAI.googleAI();

_model = aiProvider.generativeModel(
model: 'gemini-2.5-flash',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:firebase_ai/firebase_ai.dart';
import 'package:firebase_auth/firebase_auth.dart';

import '../widgets/message_widget.dart';

class ImageGenerationPage extends StatefulWidget {
Expand Down Expand Up @@ -47,9 +47,8 @@ class _ImageGenerationPageState extends State<ImageGenerationPage> {
}

void _initializeModel() {
final aiClient = widget.useVertexBackend
? FirebaseAI.vertexAI(auth: FirebaseAuth.instance)
: FirebaseAI.googleAI(auth: FirebaseAuth.instance);
final aiClient =
widget.useVertexBackend ? FirebaseAI.vertexAI() : FirebaseAI.googleAI();

_model = aiClient.generativeModel(
model: 'gemini-2.5-flash-image',
Expand Down
14 changes: 9 additions & 5 deletions packages/firebase_ai/firebase_ai/lib/src/base_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -282,19 +282,23 @@ abstract class BaseModel {
) {
return () async {
Map<String, String> headers = {};

final effectiveAppCheck = appCheck ?? app?.getService<FirebaseAppCheck>();
final effectiveAuth = auth ?? app?.getService<FirebaseAuth>();

// Override the client name in Google AI SDK
headers['x-goog-api-client'] =
'gl-dart/$packageVersion fire/$packageVersion';
if (appCheck != null) {
if (effectiveAppCheck != null) {
final appCheckToken = useLimitedUseAppCheckTokens == true
? await appCheck.getLimitedUseToken()
: await appCheck.getToken();
? await effectiveAppCheck.getLimitedUseToken()
: await effectiveAppCheck.getToken();
if (appCheckToken != null) {
headers['X-Firebase-AppCheck'] = appCheckToken;
}
}
if (auth != null) {
final idToken = await auth.currentUser?.getIdToken();
if (effectiveAuth != null) {
final idToken = await effectiveAuth.currentUser?.getIdToken();
if (idToken != null) {
headers['Authorization'] = 'Firebase $idToken';
}
Expand Down
12 changes: 12 additions & 0 deletions packages/firebase_ai/firebase_ai/lib/src/firebase_ai.dart
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,18 @@ class FirebaseAI extends FirebasePluginPlatform {
/// If pass in [appCheck], request session will get protected from abusing.
static FirebaseAI vertexAI({
FirebaseApp? app,
@Deprecated(
'Passing an explicit instance is deprecated, internal handling is now automatic.')
FirebaseAppCheck? appCheck,
@Deprecated(
'Passing an explicit instance is deprecated, internal handling is now automatic.')
FirebaseAuth? auth,
String? location,
bool? useLimitedUseAppCheckTokens,
}) {
app ??= Firebase.app();
appCheck ??= app.getService<FirebaseAppCheck>();
auth ??= app.getService<FirebaseAuth>();
var instanceKey = '${app.name}::vertexai::$location';

if (_cachedInstances.containsKey(instanceKey)) {
Expand Down Expand Up @@ -95,11 +101,17 @@ class FirebaseAI extends FirebasePluginPlatform {
/// If pass in [appCheck], request session will get protected from abusing.
static FirebaseAI googleAI({
FirebaseApp? app,
@Deprecated(
'Passing an explicit instance is deprecated, internal handling is now automatic.')
FirebaseAppCheck? appCheck,
@Deprecated(
'Passing an explicit instance is deprecated, internal handling is now automatic.')
FirebaseAuth? auth,
bool? useLimitedUseAppCheckTokens,
}) {
app ??= Firebase.app();
appCheck ??= app.getService<FirebaseAppCheck>();
auth ??= app.getService<FirebaseAuth>();
var instanceKey = '${app.name}::googleai';

if (_cachedInstances.containsKey(instanceKey)) {
Expand Down
92 changes: 92 additions & 0 deletions packages/firebase_ai/firebase_ai/test/base_model_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,20 @@ class MockFirebaseApp extends Mock implements FirebaseApp {

@override
bool get isAutomaticDataCollectionEnabled => true;

FirebaseAppCheck? mockAppCheck;
FirebaseAuth? mockAuth;

@override
T? getService<T extends FirebaseService>() {
if (T == FirebaseAppCheck) {
return mockAppCheck as T?;
}
if (T == FirebaseAuth) {
return mockAuth as T?;
}
return null;
}
}

// Mock FirebaseOptions
Expand Down Expand Up @@ -131,6 +145,84 @@ void main() {
expect(headers.length, 2);
});

test('firebaseTokens discovers App Check token dynamically at request time',
() async {
final mockApp = MockFirebaseApp();
final mockAppCheck = MockFirebaseAppCheck();
when(mockAppCheck.getToken())
.thenAnswer((_) async => 'dynamic-app-check-token');
mockApp.mockAppCheck = mockAppCheck;

final tokenFunction =
BaseModel.firebaseTokens(null, null, mockApp, false);
final headers = await tokenFunction();

expect(headers['X-Firebase-AppCheck'], 'dynamic-app-check-token');
expect(headers['X-Firebase-AppId'], 'test-app-id');
expect(headers.length, 3);
});

test('firebaseTokens discovers Auth ID token dynamically at request time',
() async {
final mockApp = MockFirebaseApp();
final mockAuth = MockFirebaseAuth();
final mockUser = MockUser();
when(mockUser.getIdToken()).thenAnswer((_) async => 'dynamic-id-token');
when(mockAuth.currentUser).thenReturn(mockUser);
mockApp.mockAuth = mockAuth;

final tokenFunction =
BaseModel.firebaseTokens(null, null, mockApp, false);
final headers = await tokenFunction();

expect(headers['Authorization'], 'Firebase dynamic-id-token');
expect(headers['X-Firebase-AppId'], 'test-app-id');
expect(headers.length, 3);
});

test('firebaseTokens discovers both tokens dynamically at request time',
() async {
final mockApp = MockFirebaseApp();
final mockAppCheck = MockFirebaseAppCheck();
final mockAuth = MockFirebaseAuth();
final mockUser = MockUser();

when(mockAppCheck.getToken())
.thenAnswer((_) async => 'dynamic-app-check-token');
when(mockUser.getIdToken()).thenAnswer((_) async => 'dynamic-id-token');
when(mockAuth.currentUser).thenReturn(mockUser);

mockApp.mockAppCheck = mockAppCheck;
mockApp.mockAuth = mockAuth;

final tokenFunction =
BaseModel.firebaseTokens(null, null, mockApp, false);
final headers = await tokenFunction();

expect(headers['X-Firebase-AppCheck'], 'dynamic-app-check-token');
expect(headers['Authorization'], 'Firebase dynamic-id-token');
expect(headers['X-Firebase-AppId'], 'test-app-id');
expect(headers.length, 4);
});

test(
'firebaseTokens discovers App Check token dynamically with limited use',
() async {
final mockApp = MockFirebaseApp();
final mockAppCheck = MockFirebaseAppCheck();

when(mockAppCheck.getLimitedUseToken())
.thenAnswer((_) async => 'dynamic-limited-use-token');
mockApp.mockAppCheck = mockAppCheck;

final tokenFunction = BaseModel.firebaseTokens(null, null, mockApp, true);
final headers = await tokenFunction();

expect(headers['X-Firebase-AppCheck'], 'dynamic-limited-use-token');
expect(headers['X-Firebase-AppId'], 'test-app-id');
expect(headers.length, 3);
});

test('firebaseTokens includes all tokens if available', () async {
final mockAppCheck = MockFirebaseAppCheck();
when(mockAppCheck.getToken())
Expand Down
17 changes: 17 additions & 0 deletions packages/firebase_ai/firebase_ai/test/firebase_vertexai_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import 'package:firebase_ai/firebase_ai.dart';
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_test/flutter_test.dart';

Expand All @@ -28,6 +29,7 @@ void main() {
late FirebaseApp customApp;
late FirebaseApp limitTokenApp;
late FirebaseAppCheck customAppCheck;
late FirebaseAuth customAuth;
late FirebaseAppCheck limitTokenAppCheck;

group('FirebaseAI Tests', () {
Expand All @@ -47,6 +49,7 @@ void main() {
appCheck = FirebaseAppCheck.instance;
customAppCheck = FirebaseAppCheck.instanceFor(app: customApp);
limitTokenAppCheck = FirebaseAppCheck.instanceFor(app: limitTokenApp);
customAuth = FirebaseAuth.instanceFor(app: customApp);
});

test('Singleton behavior', () {
Expand Down Expand Up @@ -96,6 +99,20 @@ void main() {
expect(vertexAIAppCheck.useLimitedUseAppCheckTokens, true);
});

test('Instance creation with auto-injected AppCheck', () {
final vertexAI = FirebaseAI.vertexAI(app: customApp);

expect(vertexAI.app, equals(customApp));
expect(vertexAI.appCheck, equals(customAppCheck));
});

test('Instance creation with auto-injected Auth', () {
final vertexAI = FirebaseAI.vertexAI(app: customApp);

expect(vertexAI.app, equals(customApp));
expect(vertexAI.auth, equals(customAuth));
});

Comment thread
cynthiajoan marked this conversation as resolved.
test('generativeModel creation with Grounding tools', () {
final ai = FirebaseAI.googleAI();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

part of '../firebase_app_check.dart';

class FirebaseAppCheck extends FirebasePluginPlatform {
class FirebaseAppCheck extends FirebasePluginPlatform
implements FirebaseService {
static Map<String, FirebaseAppCheck> _firebaseAppCheckInstances = {};

FirebaseAppCheck._({required this.app})
Expand Down Expand Up @@ -41,7 +42,9 @@ class FirebaseAppCheck extends FirebasePluginPlatform {
/// Returns an instance using a specified [FirebaseApp].
static FirebaseAppCheck instanceFor({required FirebaseApp app}) {
return _firebaseAppCheckInstances.putIfAbsent(app.name, () {
return FirebaseAppCheck._(app: app);
final instance = FirebaseAppCheck._(app: app);
app.registerService<FirebaseAppCheck>(instance);
return instance;
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
part of '../firebase_auth.dart';

/// The entry point of the Firebase Authentication SDK.
class FirebaseAuth extends FirebasePluginPlatform {
class FirebaseAuth extends FirebasePluginPlatform implements FirebaseService {
// Cached instances of [FirebaseAuth].
static Map<String, FirebaseAuth> _firebaseAuthInstances = {};

Expand Down Expand Up @@ -45,7 +45,9 @@ class FirebaseAuth extends FirebasePluginPlatform {
required FirebaseApp app,
}) {
return _firebaseAuthInstances.putIfAbsent(app.name, () {
return FirebaseAuth._(app: app);
final instance = FirebaseAuth._(app: app);
app.registerService<FirebaseAuth>(instance);
return instance;
});
}

Expand Down
17 changes: 17 additions & 0 deletions packages/firebase_core/firebase_core/lib/src/firebase_app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class FirebaseApp {
/// Deleting the default app is not possible and throws an exception.
Future<void> delete() async {
await _delegate.delete();
_registries.remove(name);
}

/// The name of this [FirebaseApp].
Expand Down Expand Up @@ -71,4 +72,20 @@ class FirebaseApp {

@override
String toString() => '$FirebaseApp($name)';

static final Map<String, Map<Type, dynamic>> _registries = {};
Comment thread
cynthiajoan marked this conversation as resolved.

/// Registers a service instance for this app.
void registerService<T extends FirebaseService>(T service) {
final registry = _registries.putIfAbsent(name, () => {});
registry[T] = service;
}

/// Returns a registered service instance for this app.
T? getService<T extends FirebaseService>() {
return _registries[name]?[T] as T?;
}
Comment thread
cynthiajoan marked this conversation as resolved.
}

/// A marker interface for Firebase services that can be registered in [FirebaseApp].
abstract class FirebaseService {}
Loading
Loading