llm_tool_calling_firebase_ai 0.2.0 copy "llm_tool_calling_firebase_ai: ^0.2.0" to clipboard
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:

  1. Validation: invalid arguments are sent back to Gemini as an error it can read and fix, and your function doesn't run.
  2. Confirmation: tools marked @Tool(requiresConfirmation: true) only run when confirm approves them (see below).
  3. Result: your function runs and its result goes back to Gemini. Results that aren't JSON (e.g. a DateTime) are sent as their toString().

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 as gemini-3.8-flash reject it: "Role 'function' is not supported". Your tool still runs, but the chat fails afterwards. The same applies to firebase_ai's Content.functionResponses. It's fixed in firebase_ai's source (flutterfire#18685) but not released yet. Until it is, use sendMessageWithTools (or toolResponses), which send results with the role user, 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".
  • Tool name clash: firebase_ai has a class called Tool, 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 allTools from several files); otherwise the adapter throws an ArgumentError naming 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. additionalProperties is 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, toFirebaseJsonSchema throws an ArgumentError naming 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.

0
likes
150
points
--
downloads

Documentation

API reference

Publisher

verified publisheramitgp.dev

Use llm_tool_calling tools with Firebase AI Logic (firebase_ai). Turns generated tools into Gemini function declarations and runs the whole tool loop.

Repository (GitHub)
View/report issues

Topics

#llm #tool-calling #function-calling #firebase #gemini

License

MIT (license)

Dependencies

firebase_ai, llm_tool_calling

More

Packages that depend on llm_tool_calling_firebase_ai