llm_tool_calling_firebase_ai 0.2.0
llm_tool_calling_firebase_ai: ^0.2.0 copied to clipboard
Use llm_tool_calling tools with Firebase AI Logic (firebase_ai). Turns generated tools into Gemini function declarations and runs the whole tool loop.
llm_tool_calling_firebase_ai #
Use llm_tool_calling tools
with Firebase AI Logic (firebase_ai).
Your @Tool() functions become Gemini function declarations, with no
hand-written Schema objects, and one call runs the whole tool loop.
final model = FirebaseAI.googleAI().generativeModel(
model: 'gemini-3.8-flash',
tools: [Tool.functionDeclarations(allTools.toFunctionDeclarations())],
);
final reply = await model.startChat().sendMessageWithTools(
Content.text('What is the weather in Kanpur?'),
allTools,
);
print(reply.text); // The weather in Kanpur is currently sunny and 31°C.
Install #
flutter pub add llm_tool_calling llm_tool_calling_firebase_ai firebase_ai
flutter pub add dev:llm_tool_calling_generator dev:build_runner
Write your tools and generate allTools as described in the
llm_tool_calling quick start.
Where you create the model, these imports are all you need (the adapter
also gives you ToolDefinition and ToolArgumentException):
import 'package:firebase_ai/firebase_ai.dart';
import 'package:llm_tool_calling_firebase_ai/llm_tool_calling_firebase_ai.dart';
import 'tools.dart'; // your @Tool functions and the generated allTools
Running tools #
Declare the tools with toFunctionDeclarations(), then send messages with
sendMessageWithTools. It sends your message, runs every tool Gemini asks
for, sends the results back, and repeats until Gemini answers:
final model = FirebaseAI.googleAI().generativeModel(
model: 'gemini-3.8-flash',
tools: [Tool.functionDeclarations(allTools.toFunctionDeclarations())],
);
final chat = model.startChat();
final reply = await chat.sendMessageWithTools(
Content.text('Book a flight from DEL to BOM for Asha, 30'),
allTools,
confirm: askUser, // see below
);
print(reply.text);
For each tool call:
- Validation: invalid arguments are sent back to Gemini as an error it can read and fix, and your function doesn't run.
- Confirmation: tools marked
@Tool(requiresConfirmation: true)only run whenconfirmapproves them (see below). - Result: your function runs and its result goes back to Gemini. Results
that aren't JSON (e.g. a
DateTime) are sent as theirtoString().
sendMessageWithTools stops after 10 rounds of tool calls (maxRounds), so
a confused model can't loop forever.
Tools that need confirmation #
Tools marked @Tool(requiresConfirmation: true) only run when your
confirm callback returns true. It is called after validation, so
users are never asked about a call that would fail. In a Flutter app, pass a
callback that has a BuildContext and show a dialog:
// In your widget, e.g. in a button handler:
final reply = await chat.sendMessageWithTools(
Content.text(userMessage),
allTools,
confirm: (tool, args) => askUser(context, tool, args),
);
Future<bool> askUser(
BuildContext context,
ToolDefinition tool,
Map<String, Object?> args,
) async {
final allowed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text('Allow ${tool.name}?'),
content: Text('$args'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('No'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Yes'),
),
],
),
);
return allowed ?? false;
}
Without a confirm callback these tools never run; Gemini is told the
tool needs the user's confirmation. If the user declines, Gemini is told
that too, so it can answer without the tool.
Writing the loop yourself #
sendMessageWithTools is a short loop over respondTo. To control each step
(e.g. to show progress), write it yourself:
var reply = await chat.sendMessage(Content.text('Weather in Pune?'));
while (reply.functionCalls.isNotEmpty) {
final results = [
for (final call in reply.functionCalls)
await allTools.respondTo(call, confirm: askUser),
];
reply = await chat.sendMessage(toolResponses(results));
}
print(reply.text);
Send the results with toolResponses, not firebase_ai's
Content.functionResponses; see the next section for why. respondTo never
throws for problems with the call: unknown tools, invalid arguments,
declined confirmations and exceptions from your function all become an
error Gemini can read.
firebase_ai's automatic function calling #
firebase_ai can also run tools by itself, and toFirebaseAiTool() gives it
everything it needs:
final model = FirebaseAI.googleAI().generativeModel(
model: 'gemini-3.8-flash',
tools: [allTools.toFirebaseAiTool(confirm: askUser)],
);
final response = await model.startChat().sendMessage(Content.text('Hi'));
Doesn't work with newer models in firebase_ai 4.0.0. It sends tool results with the role
function, and newer Gemini models such asgemini-3.8-flashreject it: "Role 'function' is not supported". Your tool still runs, but the chat fails afterwards. The same applies to firebase_ai'sContent.functionResponses. It's fixed in firebase_ai's source (flutterfire#18685) but not released yet. Until it is, usesendMessageWithTools(ortoolResponses), which send results with the roleuser, the same fix.
Tested with real Gemini #
Checked live in October 2026 with gemini-3.8-flash on the Gemini
Developer API, using tools made by the generator: a simple tool, a tool with
nested classes, lists and an enum (with defaults filled in) after an
approved confirmation, and the hand-written loop. Declined confirmations and
the other error paths are covered by the package's unit tests. The check app is
in the repository.
Good to know #
- App Check: firebase_ai sends an App Check token with every request. If App Check is enforced for Firebase AI Logic in your project, set App Check up in your app, or requests fail with "Firebase App Check token is invalid".
Toolname clash: firebase_ai has a class calledTool, and so does llm_tool_calling (the@Tool()annotation). Keep your@Tool()functions in their own file (e.g.tools.dart), and in the file that creates the model import only firebase_ai and this adapter, as shown above. If a file must import both packages, hide ours:import 'package:llm_tool_calling/llm_tool_calling.dart' hide Tool;.- Unique names: all tools passed together must have different names
(e.g. when combining
allToolsfrom several files); otherwise the adapter throws anArgumentErrornaming the duplicate, instead of firebase_ai silently keeping only one. - Schemas are sent as
parametersJsonSchema, the field Gemini 2.5+ uses for full JSON Schema.additionalPropertiesis left out because firebase_ai can't express it; unknown arguments are still rejected by validation. - Supported: everything llm_tool_calling generates (strings, numbers,
booleans, enums, lists and nested classes). For hand-written schemas with
other keywords,
toFirebaseJsonSchemathrows anArgumentErrornaming the unsupported part. - Tool names can be up to 63 characters, firebase_ai's limit.
Author #
Built and maintained by Amit Gupta. Bug reports, ideas and pull requests are welcome on GitHub.