genkit_firebase_ai 1.0.0
genkit_firebase_ai: ^1.0.0 copied to clipboard
Genkit plugin for Firebase AI Logic. Call Gemini from Flutter apps with tools, structured output, grounding and Live sessions.
Firebase AI plugin for Genkit Dart. It calls Gemini from Flutter apps through
Firebase AI Logic, using the
firebase_ai SDK.
Building with a coding agent? Install the Genkit Dart skill first.
npx skills add genkit-ai/skills --skill developing-genkit-dartIt teaches your agent the current Genkit Dart APIs and common gotchas. Source, manual install and skills for other languages: genkit-ai/skills.
Before you start #
- Set up Firebase AI Logic
in your Firebase project, and add Firebase to your app with
flutterfire configure. - Set up App Check.
App Check is required from November 2, 2026. From that date Firebase enforces App Check for Firebase AI Logic, and requests without a valid App Check token are blocked. That includes requests from app versions already released without App Check. You can delay enforcement in the Firebase console, but not past January 21, 2027.
Activate App Check before the first model call. The plugin reads it from the
FirebaseApp (see Configuration). Use the debug providers
during development and the platform attestation providers in release builds:
import 'package:firebase_app_check/firebase_app_check.dart';
import 'package:flutter/foundation.dart';
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
await FirebaseAppCheck.instance.activate(
providerAndroid: kDebugMode
? const AndroidDebugProvider()
: const AndroidPlayIntegrityProvider(),
providerApple: kDebugMode
? const AppleDebugProvider()
: const AppleAppAttestWithDeviceCheckFallbackProvider(),
providerWeb: kDebugMode
? WebDebugProvider()
: ReCaptchaEnterpriseProvider('your-recaptcha-site-key'),
);
Never ship the debug providers: anyone with a registered debug token passes App Check. On first run, each debug provider logs a debug token (in the browser console on web). Add it in the Firebase console under App Check > Apps > Manage debug tokens. The release providers need their own setup in the Firebase console; see Get started with App Check.
Usage #
import 'package:genkit/genkit.dart';
import 'package:genkit_firebase_ai/genkit_firebase_ai.dart';
void main() async {
// Initialize Genkit with the Firebase AI plugin
final ai = Genkit(plugins: [firebaseAI()]);
// Generate text
final response = await ai.generate(
model: firebaseAI.gemini('gemini-flash-latest'),
prompt: 'Tell me a joke about a developer.',
);
print(response.text);
}
Models #
firebaseAI.gemini(name) works with any Gemini model that Firebase AI Logic
serves. See supported models
for names and shutdown dates. The plugin also registers the models Firebase
AI Logic serves, so they show up in the Genkit Developer UI with their
capabilities:
- Text:
gemini-4-argon,gemini-3.8-flash,gemini-3.5-flash,gemini-3.5-flash-lite,gemini-3.1-flash-liteandgemini-3.1-pro-preview.gemini-flash-latest,gemini-3.7-flashandgemini-3.6-flashare registered for the Gemini Developer API only (the last two are deprecated on Agent Platform). - Image:
gemini-nano-banana-2.1,gemini-3.1-flash-image,gemini-3.1-flash-lite-imageandgemini-3-pro-image. - Text-to-speech:
gemini-3.8-flash-ttsandgemini-3.8-flash-lite-tts. - Live (see Live API):
gemini-3.8-liveon both providers, andgemini-live-2.5-flash-native-audioon Agent Platform only.
Gemini models are retired often, and a model name compiled into your app stops working when its model shuts down. For example, Gemini 2.5 Pro, Flash and Flash-Lite shut down on Agent Platform on October 20, 2026. Firebase recommends reading the model name from Remote Config, so you can change it without an app release:
import 'package:firebase_remote_config/firebase_remote_config.dart';
final remoteConfig = FirebaseRemoteConfig.instance;
await remoteConfig.setDefaults({'model_name': 'gemini-3.8-flash'});
await remoteConfig.fetchAndActivate();
final response = await ai.generate(
model: firebaseAI.gemini(remoteConfig.getString('model_name')),
prompt: 'Tell me a joke about a developer.',
);
Configuration #
You can optionally pass in FirebaseApp and FirebaseAiProvider instances,
and the useLimitedUseAppCheckTokens flag when initializing the plugin. App
Check and Auth are read from the FirebaseApp.
final firebasePlugin = firebaseAI(
app: Firebase.app('my-app'),
provider: FirebaseAiProvider.agentPlatform(),
useLimitedUseAppCheckTokens: true,
);
final ai = Genkit(plugins: [firebasePlugin]);
provider:FirebaseAiProvider.googleAI()(the default) uses the Gemini Developer API.FirebaseAiProvider.agentPlatform()uses the Agent Platform (formerly Vertex AI) Gemini API.useLimitedUseAppCheckTokens: sends a single-use App Check token with each request. You need it if you turn on replay protection, which blocks reusable session tokens.
Set location for data residency, or for Agent Platform Live models, which
need a regional location such as 'us-central1'. Text models can be missing
from a regional location and fail there with NOT_FOUND; use the default
global location for them. A plugin instance has one location, so an app that uses
both Live and text models on Agent Platform needs two Genkit instances:
final ai = Genkit(
plugins: [firebaseAI(provider: const FirebaseAiProvider.agentPlatform())],
);
final live = Genkit(
plugins: [
firebaseAI(
provider: const FirebaseAiProvider.agentPlatform(location: 'us-central1'),
),
],
);
Migrating from earlier versions:
FirebaseAiProvider.vertexAIis nowFirebaseAiProvider.agentPlatform. It defaultslocationtoglobal, notus-central1. Passlocation: 'us-central1'to keep the old behavior.- The
appCheckandauthparameters offirebaseAIare removed. The Firebase AI SDK reads App Check and Auth from theFirebaseApponly when it first builds itsFirebaseAIinstance, and it caches that instance per app (and per location foragentPlatform). It only finds services registered withFirebaseAppCheck.instanceFor(app:)andFirebaseAuth.instanceFor(app:)(or.instancefor the default app). So set up App Check and Auth for that app before the first model call. Before firebase_ai 5.1, for the same reason,useLimitedUseAppCheckTokenshas no effect if the app already created aFirebaseAIinstance for the same Firebase app and location. From 5.1 the flag is part of that cache key, so it always applies. Instances created for a different app thanapp:are no longer used. - Tool input schemas are sent unchanged as
parametersJsonSchema. They used to be converted to OpenAPI-styleparameters, which dropped keywords such asoneOf,additionalPropertiesanddefault. The model now gets the full schema, so a tool can behave differently with the same definition. - The converter and Live helper functions (
toGemini*,fromGemini*,extractUsage,runLiveSession,toLiveGenerationConfigand the like) are no longer exported. They were meant for the plugin's own use. Call models throughfirebaseAIandai.generateorai.generateBidiinstead.
To route requests through your own http.Client (a proxy, logging, retries,
or a mock in tests), pass httpClient. Live models connect over a WebSocket
and do not use it. The plugin never closes a client you provide, so close it
yourself when you are done:
import 'package:http/http.dart' as http;
final httpClient = http.Client();
final ai = Genkit(plugins: [firebaseAI(httpClient: httpClient)]);
// ...
httpClient.close();
Model options #
Pass GeminiOptions as config:
final response = await ai.generate(
model: firebaseAI.gemini('gemini-3.8-flash'),
prompt: 'Plan a three-day trip to Lisbon.',
config: GeminiOptions(
maxOutputTokens: 2048,
thinkingConfig: ThinkingConfig(thinkingLevel: 'LOW'),
),
);
Gemini 3 and newer models ignore temperature, topP and topK, and may
reject presencePenalty and frequencyPenalty with a 400 error. They don't
support candidateCount either. On older models, a non-streaming request with
candidateCount above 1 returns the first candidate as text and message
and the rest in raw; streaming with it fails with INVALID_ARGUMENT.
Thinking #
Gemini 3 and newer models take a thinkingLevel: MINIMAL, LOW, MEDIUM
or HIGH. The value must be uppercase. Which levels a model accepts depends on
the model; gemini-3.8-flash, for example, rejects MINIMAL with
INVALID_ARGUMENT. Older models take a thinkingBudget
in tokens instead. Setting both fails with INVALID_ARGUMENT before any
request is sent. Set includeThoughts: true to get the model's thought
summaries back as reasoning parts.
config: GeminiOptions(
thinkingConfig: ThinkingConfig(thinkingLevel: 'HIGH', includeThoughts: true),
),
Gemini 3 returns thought signatures, which it needs back in later turns, for
example for multi-turn tool calling. The plugin keeps each one in its part's
metadata['thoughtSignature'] and sends it back when you pass the response
messages into the next request. firebase_ai has no public API for setting a
signature, so the plugin uses its @visibleForTesting constructors for this
(flutterfire#18917). A
firebase_ai release that changes them can break the round trip.
Code execution #
codeExecution: true lets the model write and run Python. The code and its
result come back as custom parts with the keys executableCode and
codeExecutionResult.
With firebase_ai older than 5.0.0, a file part in a response (for example
a file the model generated) also comes back as a custom part, under
fileData, instead of a media part
(flutterfire#18753).
Use firebase_ai 5.0.0 or later to get it as media.
Safety settings #
safetySettings sets a blocking threshold per harm category. The
HARM_CATEGORY_IMAGE_* categories and method work on Agent Platform only.
With the Gemini Developer API, the plugin rejects them with INVALID_ARGUMENT
before sending the request.
config: GeminiOptions(
safetySettings: [
GeminiSafetySettings(
category: 'HARM_CATEGORY_HARASSMENT',
threshold: 'BLOCK_ONLY_HIGH',
),
],
),
Grounding #
googleSearch grounds answers in Google Search, urlContext lets the model
read the URLs in the prompt, and googleMaps grounds answers in Google Maps
data. For Maps, pass the user's location in retrievalConfig; a latLng with
only one coordinate fails with INVALID_ARGUMENT.
final response = await ai.generate(
model: firebaseAI.gemini('gemini-3.8-flash'),
prompt: 'Who won the most recent Formula 1 race?',
config: GeminiOptions(googleSearch: GeminiGoogleSearch()),
);
final grounding = response.custom?['groundingMetadata'] as Map?;
print(grounding?['webSearchQueries']);
for (final chunk in grounding?['groundingChunks'] as List? ?? []) {
print(chunk['web']?['uri']);
}
config: GeminiOptions(
googleMaps: GeminiGoogleMaps(),
retrievalConfig: GeminiRetrievalConfig(
latLng: GeminiLatLng(latitude: 38.72, longitude: -9.14),
),
),
The response's custom holds groundingMetadata (webSearchQueries,
groundingChunks, groundingSupports and searchEntryPoint) and, for URL
context, urlContextMetadata. Both are rebuilt from what firebase_ai
parses, so absent lists come back empty. With firebase_ai older than 5.1.0,
webSearchQueries is always empty
(flutterfire#18816).
If you use Google Search grounding,
you must
display the search suggestions and sources:
searchEntryPoint.renderedContent is HTML to show in your app.
Images and speech #
Image models return images as media parts with a data: URL. Set
responseModalities and, optionally, imageConfig:
final response = await ai.generate(
model: firebaseAI.gemini('gemini-3.1-flash-image'),
prompt: 'A watercolor of a lighthouse at dawn.',
config: GeminiOptions(
responseModalities: ['TEXT', 'IMAGE'],
imageConfig: GeminiImageConfig(aspectRatio: '16:9', imageSize: '2K'),
),
);
final image = response.media; // data:image/png;base64,...
Text-to-speech models return audio the same way. speechConfig takes one
voice, or a multiSpeakerVoiceConfig with exactly two speakers. For two
speakers, give each text part a speechMetadata with the speaker that reads
it, using a name from speakerVoiceConfigs (not the voice name). The Gemini
Developer API requires one on every text part. style is optional:
final response = await ai.generate(
model: firebaseAI.gemini('gemini-3.8-flash-tts'),
promptParts: [
TextPart(
text: 'Did we ship it?',
metadata: {'speechMetadata': {'speaker': 'Joe', 'style': 'nervous'}},
),
TextPart(
text: 'We shipped it.',
metadata: {'speechMetadata': {'speaker': 'Jane'}},
),
],
config: GeminiOptions(
responseModalities: ['AUDIO'],
speechConfig: SpeechConfig(
multiSpeakerVoiceConfig: MultiSpeakerVoiceConfig(
speakerVoiceConfigs: [
SpeakerVoiceConfig(
speaker: 'Joe',
voiceConfig: VoiceConfig(
prebuiltVoiceConfig: PrebuiltVoiceConfig(voiceName: 'Kore'),
),
),
SpeakerVoiceConfig(
speaker: 'Jane',
voiceConfig: VoiceConfig(
prebuiltVoiceConfig: PrebuiltVoiceConfig(voiceName: 'Puck'),
),
),
],
),
languageCode: 'en-US',
),
),
);
final audio = response.media; // data:audio/...;base64,...
Media resolution #
mediaResolution sets how much detail the model takes from images and video
in the prompt: MEDIA_RESOLUTION_LOW, MEDIA_RESOLUTION_MEDIUM or
MEDIA_RESOLUTION_HIGH. A media part can set its own, which also takes
MEDIA_RESOLUTION_ULTRA_HIGH:
MediaPart(
media: Media(url: 'gs://my-bucket/scan.png', contentType: 'image/png'),
metadata: {'mediaResolution': 'MEDIA_RESOLUTION_ULTRA_HIGH'},
)
Live realtime audio and video have no media resolution, so it's dropped there with a warning.
Structured output #
With outputSchema, the plugin sends your JSON schema to Gemini unchanged as
responseJsonSchema, so enum, anyOf, $ref, minimum/maximum,
additionalProperties and required all apply:
final response = await ai.generate(
model: firebaseAI.gemini('gemini-flash-latest'),
prompt: 'List three primary colors.',
outputSchema: .list(.string()),
);
print(response.output);
Tool Calling #
// Define a tool
ai.defineTool(
name: 'getWeather',
description: 'Get the weather for a location',
inputSchema: WeatherToolInput.$schema,
fn: (input, context) async {
return .response('The weather in ${input.location} is 75 and sunny.');
},
);
// Generate with tools
final response = await ai.generate(
model: firebaseAI.gemini('gemini-flash-latest'),
prompt: 'What is the weather in Boston?',
toolNames: ['getWeather'],
);
A tool's input schema goes to Gemini unchanged as parametersJsonSchema, so
enum, anyOf, $ref, minimum/maximum and additionalProperties reach
the model.
Errors #
Errors from Firebase AI Logic, the network or the SDK are reported as a
GenkitException with a status code. For example, quota errors are
RESOURCE_EXHAUSTED, an unavailable service is UNAVAILABLE, and a prompt
blocked for safety is FAILED_PRECONDITION.
ai.generate does not throw for these. It returns a response with
finishReason: FinishReason.failed, the status and message in
response.error, and the exception in response.cause. For a firebase_ai or
network error, the original exception is the cause of that exception.
final response = await ai.generate(
model: firebaseAI.gemini('gemini-flash-latest'),
prompt: 'Hello',
use: [retry(maxRetries: 2)],
);
if (response.finishReason == FinishReason.failed) {
print('${response.error?.status}: ${response.error?.message}');
}
retry retries statuses that can succeed on a second try, such as
UNAVAILABLE and RESOURCE_EXHAUSTED. A blocked prompt is not retried.
To stop a request, pass a cancellation token. Cancelling aborts the HTTP
request or stream, and the response ends with FinishReason.aborted:
final controller = CancellationController();
final result = ai.generate(
model: firebaseAI.gemini('gemini-flash-latest'),
prompt: 'Write a long story.',
cancel: controller.token,
);
// Later, for example when the user taps Stop:
controller.cancel('user pressed stop');
Live API (experimental) #
Live models stream audio and text both ways over a WebSocket, through
generateBidi. This is built on Genkit's experimental API
(package:genkit/experimental.dart), which can change in any minor release.
import 'package:genkit/experimental.dart';
final session = await ai.generateBidi(
model: 'firebaseai/gemini-3.8-live',
config: LiveGenerationConfig(
responseModalities: ['AUDIO'],
speechConfig: SpeechConfig(
voiceConfig: VoiceConfig(
prebuiltVoiceConfig: PrebuiltVoiceConfig(voiceName: 'Algenib'),
),
),
),
system: 'You are a friendly assistant.',
);
session.send('Hello');
session.stream.listen((chunk) {
// Audio media parts (data: URLs).
});
gemini-3.8-live works with both providers. On Agent Platform, Live models
need a location such as 'us-central1'; with the default global location
the server closes the session because it can't find the model. Agent Platform
also serves gemini-live-2.5-flash-native-audio.
On web, Live sessions don't work while App Check is enforced: firebase_ai
doesn't send the App Check token on the web WebSocket, so the server closes the
session with "Firebase App Check token is invalid"
(flutterfire#18938).
On Android, iOS and macOS it sends the token as a header.
A session takes system messages only in its first request (system: above);
sending one later fails with INVALID_ARGUMENT. If the server closes the
session, for example for a bad setup, the session ends with that error rather
than a normal stop.
See testapps/firebase_ai
for a complete Flutter app with tool calling, grounding and Live audio.