glosings0n / codelabs / Firebase AI Logic vs Genkit Dart vs LiteRT-LM: Hybrid AI in Flutter
50 min left
Step 1 of 8 | Architectural Context & Setup Last updated:

Firebase AI Logic vs Genkit Dart vs LiteRT-LM: Hybrid AI Architecture & Smart Routing in Flutter

1. Context & The "Why": The 2026 Flutter AI Architectural Dilemma

In 2026, Flutter engineers building generative AI applications face three distinct execution paradigms across the Google and Dart ecosystem:

  • Client-Side Cloud AI (firebase_ai): Calling gemini-flash-latest directly from Flutter without writing a custom backend, protected by Firebase App Check and Firebase Authentication.
  • Server-Side Agentic AI (genkit Dart): Running structured flows, multi-step tool calling, and privileged business logic on a Dart backend (Shelf / Cloud Run / Cloud Functions).
  • On-Device Edge AI (flutter_gemma + LiteRT-LM): Running quantized .litertlm models 100% offline on the user's NPU/GPU/CPU with zero token cost and zero network egress.

Relying on a single paradigm for your entire mobile product creates critical failure modes: a pure-cloud client breaks in rural low-connectivity zones and exposes your API quota if App Check is misconfigured; a pure-edge app cannot execute privileged server transactions or reason over massive cloud indexes; and placing sensitive billing or quota tools inside client code violates core security boundaries.

1. Contexte & Pourquoi : Le dilemme architectural de l'IA sur Flutter en 2026

En 2026, les ingénieurs Flutter concevant des applications d'IA générative disposent de trois paradigmes d'exécution complémentaires au sein de l'écosystème Google et Dart :

  • IA Cloud côté client (firebase_ai) : Appeler gemini-flash-latest directement depuis l'application Flutter sans gérer de serveur dédié, sous la protection de Firebase App Check et Firebase Authentication.
  • IA Agentique côté serveur (genkit Dart) : Exécuter des flux typés (Flows), des appels d'outils (Tool Calling) et des règles métier critiques sur un backend Dart (Shelf / Cloud Run / Cloud Functions).
  • IA Embarquée Edge (flutter_gemma + LiteRT-LM) : Exécuter des modèles quantifiés .litertlm 100% hors-ligne sur le NPU/GPU/CPU du téléphone avec un coût de token nul et zéro fuite réseau.

Utiliser un seul paradigme pour l'ensemble d'un produit mobile entraîne des défaillances majeures : une architecture 100% Cloud échoue dès que la connexion 3G/4G tombe et expose vos quotas si App Check est mal configuré ; une architecture 100% Edge ne peut pas valider des transactions serveur privilégiées ; et exécuter des outils de facturation ou de quota côté client viole les invariants de sécurité fondamentaux.

Production Architectural Pattern

Instead of choosing one SDK against the others, high-resilience mobile systems combine all three behind a unified Clean Architecture gateway and route each prompt dynamically based on connectivity, data sensitivity (PII), and transactional authority.

Plutôt que d'opposer ces trois SDK, les systèmes mobiles résilients les unifient derrière une passerelle Clean Architecture unique et routent chaque requête dynamiquement selon la connectivité, la sensibilité des données (PII) et l'autorité transactionnelle requise.

2. Learning Goals & What You Will Build

By completing this 50-minute engineering codelab, you will:

  1. Master the 3-Tier AI Decision Matrix: Evaluate latency, token unit economics per 1,000 active users, offline resilience, and security boundaries across firebase_ai, genkit, and LiteRT-LM.
  2. Enforce Absolute Server-Side Security Invariants: Understand what must never run on the client (quotas, payments, role mutations) and how to lock privileged AI tools inside a Genkit Dart server flow.
  3. Provision Firebase AI Logic Correctly via CLI: Avoid the common PERMISSION_DENIED trap by provisioning the backend service with npx firebase-tools@latest init ailogic and activating Firebase App Check.
  4. Implement a Unified SmartHybridAiRouter: Build a complete, runnable Flutter diagnostic console that routes prompts across Cloud, Server, and Edge tiers with automatic offline fallback to LiteRT-LM.

2. Objectifs & Ce que vous allez construire

Au terme de ce codelab d'ingénierie de 50 minutes, vous saurez :

  1. Maîtriser la matrice de décision IA à 3 niveaux : Évaluer la latence, le coût par 1 000 utilisateurs actifs, la résilience hors-ligne et les frontières de sécurité entre firebase_ai, genkit et LiteRT-LM.
  2. Appliquer les invariants absolus de sécurité côté serveur : Identifier ce qui ne doit jamais être décidé par le client (quotas, paiements, changements de rôles) et verrouiller les outils IA privilégiés dans un flux serveur Genkit Dart.
  3. Provisionner Firebase AI Logic via la CLI : Éviter l'erreur classique PERMISSION_DENIED en activant le service backend avec npx firebase-tools@latest init ailogic et Firebase App Check.
  4. Construire un SmartHybridAiRouter complet : Développer une console de diagnostic Flutter fonctionnelle qui route les requêtes entre Cloud, Serveur et Edge avec bascule automatique hors-ligne vers LiteRT-LM.

3. Prerequisites & System Requirements

  • Flutter & Dart SDK: Flutter >=3.29.0 and Dart >=3.8.0 (required for modern pattern matching and Genkit Dart schemas).
  • Node.js & Firebase CLI: Node.js 18+ and npx firebase-tools@latest for provisioning Firebase AI Logic.
  • Target Hardware for Edge Tier: A 64-bit physical Android device (arm64-v8a, Android 8.0+ / minSdk 26, 4GB+ RAM) or macOS/Windows desktop for running the interactive router simulation.

Run the pre-flight verification below in your terminal before creating the project:

3. Prérequis & Configuration Système

  • SDK Flutter & Dart : Flutter >=3.29.0 et Dart >=3.8.0 (requis pour le pattern matching Dart 3 et les schémas Genkit Dart).
  • Node.js & Firebase CLI : Node.js 18+ et npx firebase-tools@latest pour provisionner Firebase AI Logic.
  • Matériel cible pour le niveau Edge : Un appareil Android physique 64-bit (arm64-v8a, Android 8.0+ / minSdk 26, 4 Go+ de RAM) ou un poste macOS/Windows/Linux pour exécuter le simulateur de routage interactif.

Exécutez cette vérification préalable dans votre terminal avant d'initialiser le projet :

Terminal | Pre-Flight Toolchain Check (macOS / Linux / PowerShell) SHELL
flutter --version
dart --version
node --version
npx -y firebase-tools@latest --version
Step 2 of 8 | Architecture & Security Boundaries

Architectural Decision Matrix, Cost Modeling & Security Invariants

Before writing a single line of Dart code, every mobile architect must map out which features belong on which tier. The table below compares all three stacks across seven production engineering dimensions:

Avant d'écrire la moindre ligne de code Dart, tout architecte mobile doit déterminer quelles fonctionnalités appartiennent à quel niveau d'exécution. Le tableau ci-dessous compare les trois approches selon sept critères d'ingénierie de production :

Engineering Dimension Tier 1: Firebase AI Logic (firebase_ai) Tier 2: Genkit Dart Server (genkit) Tier 3: On-Device Edge (flutter_gemma / LiteRT-LM)
Primary Model Target gemini-flash-latest (Gemini Developer API) gemini-2.5-flash / gemini-2.5-pro gemma-3n-E2B-it-int4.litertlm
Time-to-First-Token (TTFT) 450ms to 1,800ms (network dependent) 600ms to 2,400ms (client → server → LLM) 180ms to 550ms (local NPU/GPU, 0 network)
Token Cost / 1,000 Active Users Scales linearly with prompt + media volume Scales with server compute + LLM tokens $0.00 / token forever
Offline Resilience 0% (Requires active internet connection) 0% (Requires HTTPS reachability to backend) 100% (Runs in Airplane Mode)
Security & Quota Boundary Protected by Firebase App Check + Auth Trusted Server Environment (IAM + DB rules) Zero Data Egress (PII never leaves device)
Tool Calling Authority UI-only / read-only client helper tools Privileged Mutations (Billing, Quotas, DB) Local OS actions (SQLite FTS5, Reminders, Sensors)
Device Storage / RAM Impact < 2 MB SDK footprint, 0 MB local model RAM 0 MB client model footprint 140 MB to 1.4 GB disk; 600 MB to 1.8 GB RAM
Dimension d'Ingénierie Niveau 1 : Firebase AI Logic (firebase_ai) Niveau 2 : Serveur Genkit Dart (genkit) Niveau 3 : Edge Embarqué (flutter_gemma / LiteRT-LM)
Modèle Cible Principal gemini-flash-latest (Gemini Developer API) gemini-2.5-flash / gemini-2.5-pro gemma-3n-E2B-it-int4.litertlm
Latence du 1er Token (TTFT) 450ms à 1 800ms (selon le réseau mobile) 600ms à 2 400ms (client → serveur → LLM) 180ms à 550ms (NPU/GPU local, 0 réseau)
Coût Token / 1 000 Utilisateurs Croît linéairement avec le volume de requêtes Croît avec le calcul serveur + tokens LLM 0,00 $ / token à vie
Résilience Hors-Ligne 0% (Connexion Internet requise) 0% (Accès HTTPS au serveur requis) 100% (Fonctionne en Mode Avion)
Frontière de Sécurité & Quotas Protégé par Firebase App Check + Auth Environnement Serveur de Confiance (IAM) Zéro Sortie Réseau (PII 100% locales)
Autorité de Tool Calling Outils d'interface ou de lecture côté client Mutations Privilégiées (Paiements, Quotas, DB) Actions OS locales (SQLite FTS5, Calendrier, Capteurs)
Impact Stockage / RAM Appareil < 2 Mo d'empreinte SDK, 0 Mo de RAM modèle 0 Mo d'empreinte modèle sur le client 140 Mo à 1,4 Go disque ; 600 Mo à 1,8 Go RAM

The Absolute Server-Side Security Invariant

A common anti-pattern in mobile AI apps is giving a client-side LLM tool-calling access to mutate Firestore documents directly (such as incrementing user credits, approving identity verification, or unlocking paid tiers). Even with Firebase App Check enabled, the client binary runs on untrusted hardware.

  • Rule 1 (Never Trust Client Decisions on Value): The mobile client (whether executing firebase_ai or local LiteRT-LM) must never decide subscription quotas, payment settlements, verification badges, or cross-tenant data access.
  • Rule 2 (Server Authority via Genkit Dart): Any AI workflow that triggers a state mutation with commercial, financial, or security impact must execute inside a Genkit Dart Flow on your backend, authenticating the caller's Firebase ID Token and enforcing atomic database transactions server-side.
  • Rule 3 (Zero-Egress Privacy on Edge): Conversely, when a user drafts private personal notes, medical symptoms, or confidential field logs, routing that prompt to LiteRT-LM guarantees mathematically that raw PII never leaves the physical handset.

L'Invariant Absolu de Sécurité Côté Serveur

Une erreur d'architecture fréquente dans les applications mobiles IA consiste à autoriser un modèle côté client à modifier directement des documents critiques en base de données (comme créditer un quota, valider un statut de vérification ou débloquer un abonnement). Même avec Firebase App Check activé, le binaire client s'exécute sur un matériel non fiable.

  • Règle 1 (Zéro autorité client sur la valeur métier) : Le client mobile (qu'il utilise firebase_ai ou LiteRT-LM en local) ne doit jamais décider des quotas d'abonnement, des règlements financiers, des statuts certifiés ou des accès inter-utilisateurs.
  • Règle 2 (Autorité Serveur via Genkit Dart) : Tout workflow IA déclenchant une mutation d'état à valeur commerciale ou sécuritaire doit s'exécuter dans un Flow Genkit Dart côté serveur, après vérification du jeton d'identité Firebase (ID Token) et application atomique des règles métier.
  • Règle 3 (Confidentialité sans sortie réseau sur l'Edge) : À l'inverse, lorsqu'un utilisateur traite des notes personnelles sensibles, des données médicales ou des relevés confidentiels sur le terrain, le routage vers LiteRT-LM garantit qu'aucune donnée privée (PII) ne quitte le téléphone.
Why Firebase App Check Is Mandatory for Tier 1 (firebase_ai)

While firebase_ai keeps your Gemini API key off the client device by proxying requests through the Firebase AI Logic backend, an unprotected Firebase project ID can still be abused by extracted client configs. Always activate Firebase App Check (Play Integrity on Android, DeviceCheck / App Attest on iOS) so only genuine, untampered instances of your app can consume your Gemini quota.

Bien que firebase_ai évite d'embarquer votre clé API Gemini dans l'APK en passant par le proxy Firebase AI Logic, un identifiant de projet Firebase non protégé reste vulnérable aux abus de quota. Activez systématiquement Firebase App Check (Play Integrity sur Android, DeviceCheck / App Attest sur iOS) afin que seules les instances authentiques et non altérées de votre application puissent consommer votre quota Gemini.

Step 3 of 8 | Clean Architecture Domain Layer

Project Setup & Unified Domain Layer (AiExecutionTier, RoutingContext, AiEngineGateway)

Let's create a clean Flutter project and structure our folders so the UI and application router never depend directly on SDK-specific classes from firebase_ai, genkit, or flutter_gemma.

1. Create the Project Directory Structure

Run the commands below in macOS / Linux / Git Bash or Windows PowerShell:

Créons maintenant un projet Flutter propre et structurons nos dossiers afin que l'interface utilisateur et le routeur applicatif ne dépendent jamais directement des classes spécifiques aux SDK firebase_ai, genkit ou flutter_gemma.

1. Créer l'arborescence du projet

Exécutez les commandes suivantes sous macOS / Linux / Git Bash ou Windows PowerShell :

Terminal | Create Project & Folders (Bash & PowerShell) SHELL
# 1. Create the Flutter project
flutter create hybrid_ai_router_codelab
cd hybrid_ai_router_codelab

# 2A. macOS / Linux / Git Bash: create Clean Architecture directories
mkdir -p lib/domain lib/data lib/application test

# 2B. Windows PowerShell: create Clean Architecture directories
New-Item -ItemType Directory -Force -Path lib/domain, lib/data, lib/application, test

2. Configure pubspec.yaml

Replace your pubspec.yaml with the configuration below. Notice that we use firebase_ai (which replaced the deprecated firebase_vertexai package), firebase_app_check, http for Genkit remote flow calls, and flutter_gemma for LiteRT-LM edge execution:

2. Configurer pubspec.yaml

Remplacez votre fichier pubspec.yaml par la configuration ci-dessous. Notez l'utilisation de firebase_ai (qui remplace l'ancien package déprécié firebase_vertexai), de firebase_app_check, de http pour les appels aux flux distants Genkit, et de flutter_gemma pour l'inférence LiteRT-LM embarquée :

pubspec.yaml | Hybrid AI Flutter Dependencies YAML
name: hybrid_ai_router_codelab
description: Hybrid AI Architecture & Smart Routing in Flutter (Firebase AI Logic + Genkit Dart + LiteRT-LM).
publish_to: 'none'
version: 1.0.0+1

environment:
  sdk: ^3.8.0

dependencies:
  flutter:
    sdk: flutter
  http: ^1.2.2
  # Production SDKs for real device deployment:
  # firebase_core: ^4.0.0
  # firebase_auth: ^6.0.0
  # firebase_app_check: ^0.4.0
  # firebase_ai: ^3.0.0
  # flutter_gemma: ^0.13.5

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^5.0.0
  test: ^1.25.0

flutter:
  uses-material-design: true

3. Create the Unified Domain Contract (lib/domain/ai_models.dart)

Create lib/domain/ai_models.dart. This file defines our three execution tiers, the privacy sensitivity levels, the runtime routing context, telemetry metrics, and the AiEngineGateway interface implemented by all three engines:

3. Créer le Contrat de Domaine Unifié (lib/domain/ai_models.dart)

Créez le fichier lib/domain/ai_models.dart. Ce fichier définit nos trois niveaux d'exécution, la classification de sensibilité des données, le contexte d'évaluation du routeur, la télémétrie et l'interface AiEngineGateway implémentée par les trois moteurs :

lib/domain/ai_models.dart | Unified Domain Models & Gateway DART
// lib/domain/ai_models.dart

/// The three execution tiers available in a 2026 Hybrid Flutter AI architecture.
enum AiExecutionTier {
  /// Tier 1: Direct client-to-Gemini calls via `firebase_ai` + App Check.
  cloudFirebaseAi(
    label: 'Cloud (Firebase AI Logic)',
    modelId: 'gemini-flash-latest',
    zeroTokenCost: false,
  ),

  /// Tier 2: Server-side agentic flows via Genkit Dart + Shelf / Cloud Run.
  serverGenkitAgent(
    label: 'Server (Genkit Dart Agent)',
    modelId: 'gemini-2.5-flash',
    zeroTokenCost: false,
  ),

  /// Tier 3: 100% offline on-device inference via `flutter_gemma` + LiteRT-LM.
  edgeLiteRt(
    label: 'On-Device (LiteRT-LM INT4)',
    modelId: 'gemma-3n-E2B-it-int4.litertlm',
    zeroTokenCost: true,
  );

  const AiExecutionTier({
    required this.label,
    required this.modelId,
    required this.zeroTokenCost,
  });

  final String label;
  final String modelId;
  final bool zeroTokenCost;
}

/// Data privacy classification for outgoing prompts.
enum TaskSensitivity {
  /// Public or general knowledge queries safe for cloud transit.
  publicGeneral,

  /// Contains unredacted PII, medical records, or strictly private local notes.
  /// Must never leave the physical device.
  strictLocalPii,

  /// Requires privileged server-side authority (billing, quotas, DB mutations).
  privilegedTransactional,
}

/// Runtime context evaluated by the SmartHybridAiRouter before dispatching.
class RoutingContext {
  const RoutingContext({
    required this.prompt,
    required this.isOnline,
    this.sensitivity = TaskSensitivity.publicGeneral,
    this.requiresPrivilegedServerTool = false,
    this.hasMultimodalAttachment = false,
    this.availableRamMb = 4096,
  });

  final String prompt;
  final bool isOnline;
  final TaskSensitivity sensitivity;
  final bool requiresPrivilegedServerTool;
  final bool hasMultimodalAttachment;
  final int availableRamMb;
}

/// Telemetry metadata attached to every completed AI inference.
class RoutingTelemetry {
  const RoutingTelemetry({
    required this.selectedTier,
    required this.routingReason,
    required this.latencyMs,
    this.didFallbackToEdge = false,
  });

  final AiExecutionTier selectedTier;
  final String routingReason;
  final int latencyMs;
  final bool didFallbackToEdge;
}

/// Unified response returned to the presentation layer regardless of tier.
class AiInferenceResponse {
  const AiInferenceResponse({
    required this.text,
    required this.telemetry,
    this.structuredPayload,
  });

  final String text;
  final RoutingTelemetry telemetry;
  final Map<String, Object?>? structuredPayload;
}

/// Unified gateway implemented by Cloud, Server, and Edge engines.
abstract interface class AiEngineGateway {
  AiExecutionTier get tier;

  Future<String> generate({
    required String prompt,
    Map<String, Object?> metadata = const {},
  });
}
Step 4 of 8 | Tier 1: Client-Side Cloud AI

Tier 1 | Client-Side Cloud Inference with firebase_ai & Firebase App Check

Firebase AI Logic (package:firebase_ai) lets your Flutter app call Gemini models directly when the device is online and the task does not involve privileged server state mutations. It shines for multimodal inputs (analyzing high-resolution photos, audio clips, or PDFs up to 20 MB inline) and general multi-turn chat.

1. Provisioning the Backend Service (Avoiding PERMISSION_DENIED)

A critical mistake many Flutter developers make is running flutterfire configure and assuming Firebase AI Logic is ready. flutterfire configure only generates client configuration (firebase_options.dart); it does NOT enable the Gemini Developer API proxy on your Firebase project. You must run the Firebase CLI init ailogic command first:

Firebase AI Logic (package:firebase_ai) permet à votre application Flutter d'appeler directement les modèles Gemini lorsque l'appareil est connecté et que la tâche n'implique pas de mutations d'état serveur privilégiées. Ce niveau excelle pour les entrées multimodales (analyse de photos haute résolution, d'extraits audio ou de PDF jusqu'à 20 Mo) et le chat généraliste.

1. Provisionner le Service Backend (Éviter PERMISSION_DENIED)

Une erreur fréquente chez les développeurs Flutter consiste à exécuter uniquement flutterfire configure en pensant que Firebase AI Logic est activé. flutterfire configure génère uniquement le fichier client firebase_options.dart ; il n'active PAS le proxy Gemini Developer API sur votre projet Firebase. Vous devez obligatoirement exécuter la commande init ailogic via la CLI Firebase :

Terminal | Provision Firebase AI Logic & Client Options SHELL
# 1. Verify active Firebase project and registered apps
npx -y firebase-tools@latest projects:list
npx -y firebase-tools@latest apps:list

# 2. MANDATORY: Provision Firebase AI Logic (enables Gemini Developer API)
npx -y firebase-tools@latest init ailogic

# 3. Generate Flutter client configuration (lib/firebase_options.dart)
flutterfire configure
Model Selection Rule: Always Use gemini-flash-latest

Never hardcode legacy gemini-1.5-flash identifiers in new Flutter projects. Always target gemini-flash-latest via FirebaseAI.googleAI(auth: FirebaseAuth.instance), and optionally bind the model string to Firebase Remote Config so you can roll out model upgrades without waiting for Play Store or App Store review.

N'utilisez jamais les anciens identifiants obsolètes comme gemini-1.5-flash. Ciblez systématiquement gemini-flash-latest via FirebaseAI.googleAI(auth: FirebaseAuth.instance), et liez idéalement le nom du modèle à Firebase Remote Config afin de pouvoir mettre à jour la version du modèle à distance sans republier sur le Play Store ou l'App Store.

2. Implement lib/data/firebase_ai_cloud_engine.dart

Create lib/data/firebase_ai_cloud_engine.dart. We inject a CloudPromptInvoker delegate so that in production you pass the live FirebaseAI.googleAI(auth: FirebaseAuth.instance) call, while in local unit tests or codelab simulation mode it runs out-of-the-box without requiring live cloud credentials:

2. Implémenter lib/data/firebase_ai_cloud_engine.dart

Créez le fichier lib/data/firebase_ai_cloud_engine.dart. Nous injectons un délégué CloudPromptInvoker : en production, vous lui passez l'appel réel FirebaseAI.googleAI(auth: FirebaseAuth.instance) après activation d'App Check, tandis qu'en test unitaire ou dans le simulateur du codelab, il s'exécute immédiatement sans nécessiter d'identifiants Cloud :

lib/data/firebase_ai_cloud_engine.dart | Tier 1 Cloud Engine DART
// lib/data/firebase_ai_cloud_engine.dart
import 'dart:async';
import '../domain/ai_models.dart';

/// Signature for invoking `FirebaseAI.googleAI().generativeModel(...)`.
typedef CloudPromptInvoker = Future<String> Function(
  String modelName,
  String systemInstruction,
  String userPrompt,
);

/// Tier 1 Engine: Client-Side Cloud Inference via Firebase AI Logic.
///
/// Production wiring with `package:firebase_ai/firebase_ai.dart`:
/// ```dart
/// await FirebaseAppCheck.instance.activate(
///   androidProvider: AndroidProvider.playIntegrity,
///   appleProvider: AppleProvider.appAttest,
/// );
/// final googleAI = FirebaseAI.googleAI(auth: FirebaseAuth.instance);
/// final model = googleAI.generativeModel(
///   model: 'gemini-flash-latest',
///   systemInstruction: Content.system(systemInstruction),
/// );
/// final res = await model.generateContent([Content.text(userPrompt)]);
/// return res.text ?? '';
/// ```
class FirebaseAiCloudEngine implements AiEngineGateway {
  FirebaseAiCloudEngine({
    CloudPromptInvoker? invoker,
    this.modelName = 'gemini-flash-latest',
    this.timeout = const Duration(seconds: 12),
  }) : _invoker = invoker ?? _defaultSimulatedCloudInvoker;

  final CloudPromptInvoker _invoker;
  final String modelName;
  final Duration timeout;

  static const String _systemPrompt =
      'You are a concise engineering assistant accessed via Firebase AI Logic. '
      'Never claim authority to mutate billing quotas or database roles directly.';

  @override
  AiExecutionTier get tier => AiExecutionTier.cloudFirebaseAi;

  @override
  Future<String> generate({
    required String prompt,
    Map<String, Object?> metadata = const {},
  }) async {
    final trimmed = prompt.trim();
    if (trimmed.isEmpty) {
      throw ArgumentError('Prompt cannot be empty for Firebase AI Logic.');
    }

    final responseText = await _invoker(
      modelName,
      _systemPrompt,
      trimmed,
    ).timeout(timeout);

    if (responseText.trim().isEmpty) {
      throw StateError('Firebase AI Logic ($modelName) returned an empty text payload.');
    }
    return responseText.trim();
  }

  static Future<String> _defaultSimulatedCloudInvoker(
    String model,
    String systemInstruction,
    String prompt,
  ) async {
    await Future<void>.delayed(const Duration(milliseconds: 320));
    return '[Firebase AI Logic | $model] Processed multimodal/cloud prompt '
        'with App Check attestation verified: "$prompt"';
  }
}
Step 5 of 8 | Tier 2: Privileged Server Agent

Tier 2 | Server-Side Agentic Workflows with Genkit Dart Remote Actions

When an AI action requires reading confidential cross-tenant metrics, verifying payment status, or mutating organization quotas, it must run on Tier 2: Genkit Dart Server. With package:genkit and package:genkit_google_genai, you write both your backend AI flows and your Flutter app in 100% type-safe Dart.

1. How the Backend Flow Looks in Genkit Dart

Below is the reference architecture for a Genkit Dart server flow using ai.defineTool and ai.defineFlow. Notice how the privileged quota check executes inside the server boundary before the model can invoke the tool:

Lorsqu'une action IA nécessite de lire des métriques confidentielles, de vérifier un paiement ou de modifier les quotas d'une organisation, elle doit impérativement s'exécuter sur le Niveau 2 : Serveur Genkit Dart. Grâce à package:genkit et package:genkit_google_genai, vous écrivez vos flux IA backend et votre application Flutter dans un langage Dart 100% typé de bout en bout.

1. Architecture de référence du Flow Backend en Genkit Dart

Voici l'architecture d'un Flow serveur Genkit Dart utilisant ai.defineTool et ai.defineFlow. Remarquez comment le contrôle de sécurité et de quota s'exécute à l'intérieur du périmètre serveur avant toute exécution d'outil privilégié :

server/bin/genkit_quota_flow.dart | Server-Side Genkit Flow Reference DART
// Reference Backend File (deployed to Cloud Run / Dart Shelf Server)
import 'package:genkit/genkit.dart';
import 'package:genkit_google_genai/genkit_google_genai.dart';
import 'package:schemantic/schemantic.dart';

void configureServerFlows() {
  final ai = Genkit(plugins: [googleAI()]);

  // Privileged server-only tool: never exposed to direct client manipulation.
  ai.defineTool(
    name: 'verifyAndAllocateOrganizationQuota',
    description: 'Verifies billing ledger state and allocates monthly AI compute quota.',
    inputSchema: SchemanticType.map(.string(), .string()),
    fn: (input, _) async {
      final orgId = input['orgId'] ?? 'unknown';
      // Server-side invariant: read authoritative ledger in Admin SDK / Postgres.
      return 'Ledger verified for $orgId: 50,000 enterprise tokens allocated.';
    },
  );

  ai.defineFlow(
    name: 'privilegedQuotaAgentFlow',
    inputSchema: .string(),
    outputSchema: .string(),
    fn: (prompt, _) async {
      final response = await ai.generate(
        model: googleAI.gemini('gemini-2.5-flash'),
        prompt: prompt,
        toolNames: ['verifyAndAllocateOrganizationQuota'],
      );
      return response.text;
    },
  );
}

2. Implement the Flutter Client Adapter (lib/data/genkit_server_engine.dart)

On the Flutter client, you can invoke a deployed Genkit flow either via defineRemoteAction from package:genkit/client.dart or via an authenticated HTTPS POST carrying the user's Firebase Auth ID Token in the Authorization: Bearer <token> header. Create lib/data/genkit_server_engine.dart:

2. Implémenter l'Adaptateur Client Flutter (lib/data/genkit_server_engine.dart)

Côté client Flutter, vous pouvez invoquer un flux Genkit déployé soit via defineRemoteAction (depuis package:genkit/client.dart), soit via une requête HTTPS POST authentifiée transmettant le jeton Firebase Auth dans l'en-tête Authorization: Bearer <token>. Créez le fichier lib/data/genkit_server_engine.dart :

lib/data/genkit_server_engine.dart | Tier 2 Genkit Remote Flow Client DART
// lib/data/genkit_server_engine.dart
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../domain/ai_models.dart';

/// Tier 2 Engine: Server-Side Agentic Workflows via Genkit Dart Remote Actions.
///
/// Enforces the Absolute Server-Side Security Invariant:
/// Any operation touching user quotas, payment ledgers, or privileged tools
/// executes exclusively on the Dart backend after verifying the Firebase ID Token.
class GenkitServerEngine implements AiEngineGateway {
  GenkitServerEngine({
    required this.flowEndpoint,
    required this.idTokenProvider,
    http.Client? httpClient,
    this.timeout = const Duration(seconds: 15),
  }) : _httpClient = httpClient;

  final Uri flowEndpoint;
  final Future<String> Function() idTokenProvider;
  final http.Client? _httpClient;
  final Duration timeout;

  @override
  AiExecutionTier get tier => AiExecutionTier.serverGenkitAgent;

  @override
  Future<String> generate({
    required String prompt,
    Map<String, Object?> metadata = const {},
  }) async {
    final idToken = await idTokenProvider();
    if (idToken.isEmpty) {
      throw StateError('Unauthenticated client: missing Firebase Auth ID token.');
    }

    // If no live HTTP client is injected, run deterministic local verification
    // of the Genkit Flow JSON envelope contract.
    if (_httpClient == null) {
      await Future<void>.delayed(const Duration(milliseconds: 410));
      return '[Genkit Dart Server | gemini-2.5-flash] Server-side security '
          'invariant verified (Bearer token + quota ledger check passed). '
          'Executed privileged workflow for: "${prompt.trim()}"';
    }

    final response = await _httpClient
        .post(
          flowEndpoint,
          headers: <String, String>{
            'Content-Type': 'application/json',
            'Authorization': 'Bearer $idToken',
          },
          body: jsonEncode(<String, Object?>{
            'data': <String, Object?>{
              'prompt': prompt.trim(),
              'metadata': metadata,
            },
          }),
        )
        .timeout(timeout);

    if (response.statusCode != 200) {
      throw StateError(
        'Genkit flow failed with HTTP ${response.statusCode}: ${response.body}',
      );
    }

    final decoded = jsonDecode(response.body) as Map<String, Object?>;
    final result = decoded['result']?.toString() ?? '';
    if (result.isEmpty) {
      throw StateError('Genkit flow returned an empty result field.');
    }
    return result;
  }
}
Step 6 of 8 | Tier 3: Zero-Egress On-Device AI

Tier 3 | 100% Offline Edge Inference with flutter_gemma & LiteRT-LM

What happens when your user enters an elevator, travels into a rural agricultural zone with zero cellular coverage, or types a strictly private note containing sensitive PII? Our third tier, LiteRT-LM via flutter_gemma, executes a 4-bit quantized Gemma model directly on the device's NPU/GPU/CPU.

Three Production Guardrails for On-Device LiteRT-LM

  1. Strict FFI Mutex Serialization: The underlying C++ LiteRT-LM / MediaPipe session is not thread-safe for concurrent prompt streams. Always serialize access with a Dart Completer<void> lock.
  2. Deadlock Timeout Recovery: If a developer triggers a Flutter Hot Restart while a native C++ inference is in flight, a naive mutex would block forever. Always attach a timeout (e.g., 25 seconds) that resets the lock cleanly.
  3. Low-RAM Deterministic Guard (< 2GB Available RAM): Before allocating native KV-cache tensors on budget devices, inspect available memory and fall back to a zero-RAM deterministic ruleset if the OS is under extreme memory pressure.

Create lib/data/litert_edge_engine.dart:

Que se passe-t-il lorsque votre utilisateur perd le réseau mobile en zone rurale, passe en mode avion, ou rédige une note confidentielle contenant des données personnelles sensibles (PII) ? Notre troisième niveau, LiteRT-LM via flutter_gemma, exécute un modèle Gemma quantifié en 4 bits (INT4) directement sur le NPU/GPU/CPU de l'appareil.

Trois Garde-Fous de Production pour LiteRT-LM Embarqué

  1. Sérialisation stricte par Mutex FFI : La session native C++ LiteRT-LM / MediaPipe ne supporte pas deux flux d'inférence simultanés. Sérialisez systématiquement les appels avec un verrou Dart Completer<void>.
  2. Récupération anti-deadlock par Timeout : Si un Hot Restart survient pendant une inférence C++ native, un mutex classique bloquerait l'application indéfiniment. Ajoutez toujours un délai d'expiration (ex. 25 secondes) pour libérer le verrou proprement.
  3. Garde-fou déterministe Basse-RAM (< 2 Go de RAM libre) : Sur les téléphones d'entrée de gamme soumis à une forte pression mémoire, vérifiez la RAM disponible avant d'allouer le cache KV natif et basculez vers un moteur de règles local sans surcoût mémoire.

Créez le fichier lib/data/litert_edge_engine.dart :

lib/data/litert_edge_engine.dart | Tier 3 LiteRT-LM Engine with Mutex & RAM Guard DART
// lib/data/litert_edge_engine.dart
import 'dart:async';
import '../domain/ai_models.dart';

/// Signature for invoking the native C++ LiteRT-LM session via `flutter_gemma`.
typedef LiteRtNativeInvoker = Future<String> Function(String prompt);

/// Tier 3 Engine: 100% Offline On-Device Inference via LiteRT-LM (`flutter_gemma`).
///
/// Guarantees:
/// 1. Strict mutex serialization so two concurrent UI requests never collide
///    inside the native C++ MediaPipe / LiteRT-LM session.
/// 2. Deadlock recovery timeout so a stalled native session releases the lock.
/// 3. Zero-RAM deterministic fallback on ultra-low-memory devices (<2GB RAM).
class LiteRtEdgeEngine implements AiEngineGateway {
  LiteRtEdgeEngine({
    LiteRtNativeInvoker? nativeInvoker,
    this.lockTimeout = const Duration(seconds: 25),
  }) : _nativeInvoker = nativeInvoker ?? _defaultSimulatedLiteRtInvoker;

  final LiteRtNativeInvoker _nativeInvoker;
  final Duration lockTimeout;
  Completer<void>? _activeLock;

  @override
  AiExecutionTier get tier => AiExecutionTier.edgeLiteRt;

  @override
  Future<String> generate({
    required String prompt,
    Map<String, Object?> metadata = const {},
  }) async {
    final availableRamMb = (metadata['availableRamMb'] as int?) ?? 4096;
    if (availableRamMb < 2048) {
      return _deterministicLowRamFallback(prompt);
    }

    return _runLocked<String>(() async {
      final formattedPrompt =
          '<start_of_turn>user\n${prompt.trim()}<end_of_turn>\n'
          '<start_of_turn>model\n';
      return _nativeInvoker(formattedPrompt);
    });
  }

  Future<T> _runLocked<T>(Future<T> Function() action) async {
    while (_activeLock != null) {
      try {
        await _activeLock!.future.timeout(lockTimeout);
      } on TimeoutException {
        // Recover from a stalled FFI session after Hot Restart or native hang.
        _activeLock = null;
        break;
      }
    }

    final completer = Completer<void>();
    _activeLock = completer;

    try {
      return await action();
    } finally {
      _activeLock = null;
      if (!completer.isCompleted) {
        completer.complete();
      }
    }
  }

  String _deterministicLowRamFallback(String prompt) {
    return '[LiteRT-LM Safe Mode | <2GB RAM Guard] Device memory is critically '
        'low. Returning deterministic local ruleset summary for: "${prompt.trim()}"';
  }

  static Future<String> _defaultSimulatedLiteRtInvoker(
    String formattedPrompt,
  ) async {
    await Future<void>.delayed(const Duration(milliseconds: 190));
    final clean = formattedPrompt
        .replaceAll('<start_of_turn>user\n', '')
        .replaceAll('<end_of_turn>\n<start_of_turn>model\n', '');
    return '[LiteRT-LM On-Device | gemma-3n-E2B-it-int4] 100% offline inference '
        '(0 bytes network egress, \$0.00 token cost): "$clean"';
  }
}
Deep-Dive into On-Device Quantization & SQLite FTS5 RAG

Want to see how to stream .litertlm model downloads atomically, resize 512px vision tensors, and pair LiteRT-LM with zero-RAM SQLite FTS5 BM25 RAG? Check out our companion codelab: On-Device AI with Flutter & Gemma (droidCon Uganda 2026) and the public reference repository github.com/glosings0n/gemmea.

Vous souhaitez approfondir le téléchargement atomique de modèles .litertlm, le redimensionnement d'images en 512px et le RAG local SQLite FTS5 BM25 sans impact RAM ? Consultez notre codelab dédié : On-Device AI with Flutter & Gemma (droidCon Uganda 2026) ainsi que le dépôt public de référence github.com/glosings0n/gemmea.

Step 7 of 8 | Dynamic Orchestration & Presentation

The SmartHybridAiRouter & Interactive Flutter Diagnostic Console

Now we connect all three engines inside lib/application/smart_hybrid_ai_router.dart. The router evaluates five ordered rules for every outgoing prompt and automatically falls back to on-device LiteRT-LM if a cloud or server request fails mid-flight due to unstable mobile connectivity.

1. Create lib/application/smart_hybrid_ai_router.dart

Connectons à présent nos trois moteurs au sein de lib/application/smart_hybrid_ai_router.dart. Le routeur évalue cinq règles ordonnées pour chaque requête sortante et bascule automatiquement vers le moteur embarqué LiteRT-LM si un appel Cloud ou Serveur échoue en cours de route à cause d'une coupure réseau.

1. Créer lib/application/smart_hybrid_ai_router.dart

lib/application/smart_hybrid_ai_router.dart | 5-Rule Hybrid Router + Auto Fallback DART
// lib/application/smart_hybrid_ai_router.dart
import '../domain/ai_models.dart';

/// Evaluates runtime constraints (connectivity, PII sensitivity, server authority,
/// and hardware limits) to dispatch requests across Cloud, Server, and Edge AI.
class SmartHybridAiRouter {
  const SmartHybridAiRouter({
    required AiEngineGateway cloudEngine,
    required AiEngineGateway serverEngine,
    required AiEngineGateway edgeEngine,
  })  : _cloudEngine = cloudEngine,
        _serverEngine = serverEngine,
        _edgeEngine = edgeEngine;

  final AiEngineGateway _cloudEngine;
  final AiEngineGateway _serverEngine;
  final AiEngineGateway _edgeEngine;

  /// Selects the target tier and reason before executing inference.
  (AiEngineGateway, String) selectTargetEngine(RoutingContext context) {
    // Rule 1: Strict PII or confidential local notes NEVER leave the device.
    if (context.sensitivity == TaskSensitivity.strictLocalPii) {
      return (
        _edgeEngine,
        'Strict PII policy: zero network egress enforced via on-device LiteRT-LM.',
      );
    }

    // Rule 2: Privileged tools, billing quotas, or state mutations require Server authority.
    if (context.requiresPrivilegedServerTool ||
        context.sensitivity == TaskSensitivity.privilegedTransactional) {
      if (!context.isOnline) {
        return (
          _edgeEngine,
          'Offline during privileged request: queued transaction & serving local LiteRT-LM guidance.',
        );
      }
      return (
        _serverEngine,
        'Server-Side Security Invariant: routed to Genkit Dart backend for privileged tool execution.',
      );
    }

    // Rule 3: Offline mode routes immediately to on-device LiteRT-LM.
    if (!context.isOnline) {
      return (
        _edgeEngine,
        'Device is offline: routed to local LiteRT-LM INT4 engine (\$0.00 token cost).',
      );
    }

    // Rule 4: Online + Multimodal attachment -> Firebase AI Logic.
    if (context.hasMultimodalAttachment) {
      return (
        _cloudEngine,
        'Online multimodal input: routed to Firebase AI Logic (gemini-flash-latest + App Check).',
      );
    }

    // Rule 5: Short, routine prompts on capable devices (>=4GB RAM) run locally to save cloud quota.
    if (context.prompt.length < 160 && context.availableRamMb >= 4096) {
      return (
        _edgeEngine,
        'Cost optimization: short prompt on 4GB+ RAM device offloaded to local LiteRT-LM.',
      );
    }

    return (
      _cloudEngine,
      'Online complex synthesis: routed to Firebase AI Logic (gemini-flash-latest).',
    );
  }

  /// Dispatches the prompt and automatically falls back to LiteRT-LM on network failure.
  Future<AiInferenceResponse> routeAndExecute(RoutingContext context) async {
    final stopwatch = Stopwatch()..start();
    final (primaryEngine, reason) = selectTargetEngine(context);

    try {
      final output = await primaryEngine.generate(
        prompt: context.prompt,
        metadata: <String, Object?>{
          'availableRamMb': context.availableRamMb,
          'hasMultimodalAttachment': context.hasMultimodalAttachment,
        },
      );
      stopwatch.stop();

      return AiInferenceResponse(
        text: output,
        telemetry: RoutingTelemetry(
          selectedTier: primaryEngine.tier,
          routingReason: reason,
          latencyMs: stopwatch.elapsedMilliseconds,
        ),
      );
    } catch (cloudOrServerError) {
      // If the primary engine was Cloud or Server and failed due to flaky network,
      // transparently fall back to on-device LiteRT-LM so UX never breaks.
      if (primaryEngine.tier != AiExecutionTier.edgeLiteRt) {
        final fallbackOutput = await _edgeEngine.generate(
          prompt: context.prompt,
          metadata: <String, Object?>{
            'availableRamMb': context.availableRamMb,
          },
        );
        stopwatch.stop();

        return AiInferenceResponse(
          text: fallbackOutput,
          telemetry: RoutingTelemetry(
            selectedTier: AiExecutionTier.edgeLiteRt,
            routingReason:
                'Automatic Edge Fallback after ${primaryEngine.tier.label} error: $cloudOrServerError',
            latencyMs: stopwatch.elapsedMilliseconds,
            didFallbackToEdge: true,
          ),
        );
      }
      rethrow;
    }
  }
}

2. Build the Interactive Diagnostic Console (lib/main.dart)

Replace lib/main.dart with the complete Flutter application below. It provides interactive switches to toggle connectivity (Online vs. Airplane Mode), PII sensitivity, Privileged Genkit Server tools, Multimodal attachments, and available RAM, displaying the selected tier, routing justification, and latency in real time:

2. Construire la Console de Diagnostic Interactive (lib/main.dart)

Remplacez lib/main.dart par l'application Flutter complète ci-dessous. Elle offre des commutateurs interactifs permettant de simuler la connectivité (En ligne vs Mode Avion), la sensibilité PII, les outils serveur privilégiés Genkit, les pièces jointes multimodales et la RAM disponible, tout en affichant en temps réel le niveau sélectionné, la justification du routage et la latence :

lib/main.dart | Complete Runnable Hybrid AI Diagnostic Console DART
// lib/main.dart
import 'package:flutter/material.dart';
import 'application/smart_hybrid_ai_router.dart';
import 'data/firebase_ai_cloud_engine.dart';
import 'data/genkit_server_engine.dart';
import 'data/litert_edge_engine.dart';
import 'domain/ai_models.dart';

void main() {
  runApp(const HybridAiCodelabApp());
}

class HybridAiCodelabApp extends StatelessWidget {
  const HybridAiCodelabApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Hybrid AI Router Codelab',
      debugShowCheckedModeBanner: false,
      theme: ThemeData.dark(useMaterial3: true).copyWith(
        scaffoldBackgroundColor: const Color(0xFF070A13),
        colorScheme: const ColorScheme.dark(
          primary: Color(0xFF00B0FF),
          secondary: Color(0xFF02569B),
          surface: Color(0xFF0F1424),
        ),
      ),
      home: const HybridRouterConsoleScreen(),
    );
  }
}

class HybridRouterConsoleScreen extends StatefulWidget {
  const HybridRouterConsoleScreen({super.key});

  @override
  State<HybridRouterConsoleScreen> createState() =>
      _HybridRouterConsoleScreenState();
}

class _HybridRouterConsoleScreenState extends State<HybridRouterConsoleScreen> {
  late final SmartHybridAiRouter _router;
  final TextEditingController _promptController = TextEditingController(
    text: 'Summarize field inspection metrics and recommend next action.',
  );

  bool _isOnline = true;
  bool _containsStrictPii = false;
  bool _requiresPrivilegedServerTool = false;
  bool _hasMultimodalAttachment = false;
  int _availableRamMb = 4096;
  bool _isBusy = false;
  AiInferenceResponse? _lastResponse;

  @override
  void initState() {
    super.initState();
    _router = SmartHybridAiRouter(
      cloudEngine: FirebaseAiCloudEngine(),
      serverEngine: GenkitServerEngine(
        flowEndpoint: Uri.parse('https://api.example.com/privilegedQuotaAgentFlow'),
        idTokenProvider: () async => 'demo-firebase-id-token',
      ),
      edgeEngine: LiteRtEdgeEngine(),
    );
  }

  @override
  void dispose() {
    _promptController.dispose();
    super.dispose();
  }

  Future<void> _dispatchPrompt() async {
    setState(() => _isBusy = true);
    try {
      final contextModel = RoutingContext(
        prompt: _promptController.text,
        isOnline: _isOnline,
        sensitivity: _containsStrictPii
            ? TaskSensitivity.strictLocalPii
            : (_requiresPrivilegedServerTool
                ? TaskSensitivity.privilegedTransactional
                : TaskSensitivity.publicGeneral),
        requiresPrivilegedServerTool: _requiresPrivilegedServerTool,
        hasMultimodalAttachment: _hasMultimodalAttachment,
        availableRamMb: _availableRamMb,
      );

      final result = await _router.routeAndExecute(contextModel);
      setState(() => _lastResponse = result);
    } finally {
      setState(() => _isBusy = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    final telemetry = _lastResponse?.telemetry;
    return Scaffold(
      appBar: AppBar(
        title: const Text('Hybrid AI Smart Router Console'),
        backgroundColor: const Color(0xFF0F1424),
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          SwitchListTile(
            title: const Text('Network Connectivity (Online)'),
            subtitle: const Text('Disable to simulate rural offline / Airplane mode'),
            value: _isOnline,
            onChanged: (v) => setState(() => _isOnline = v),
          ),
          SwitchListTile(
            title: const Text('Strict Local PII / Confidential Notes'),
            subtitle: const Text('Forces zero-egress on-device LiteRT-LM execution'),
            value: _containsStrictPii,
            onChanged: (v) => setState(() => _containsStrictPii = v),
          ),
          SwitchListTile(
            title: const Text('Privileged Server Tool (Billing / Quota / DB)'),
            subtitle: const Text('Enforces Genkit Dart Server-Side Security Invariant'),
            value: _requiresPrivilegedServerTool,
            onChanged: (v) => setState(() => _requiresPrivilegedServerTool = v),
          ),
          SwitchListTile(
            title: const Text('High-Res Multimodal Attachment'),
            subtitle: const Text('Routes online media to Firebase AI Logic (gemini-flash-latest)'),
            value: _hasMultimodalAttachment,
            onChanged: (v) => setState(() => _hasMultimodalAttachment = v),
          ),
          ListTile(
            title: Text('Simulated Device RAM: $_availableRamMb MB'),
            subtitle: Slider(
              min: 1536,
              max: 8192,
              divisions: 13,
              value: _availableRamMb.toDouble(),
              onChanged: (v) => setState(() => _availableRamMb = v.round()),
            ),
          ),
          const SizedBox(height: 8),
          TextField(
            controller: _promptController,
            maxLines: 3,
            decoration: const InputDecoration(
              labelText: 'Prompt Payload',
              border: OutlineInputBorder(),
            ),
          ),
          const SizedBox(height: 12),
          FilledButton(
            onPressed: _isBusy ? null : _dispatchPrompt,
            child: Text(_isBusy ? 'Routing & Executing...' : 'Route & Execute Prompt'),
          ),
          if (telemetry != null) ...[
            const SizedBox(height: 20),
            Container(
              padding: const EdgeInsets.all(16),
              decoration: BoxDecoration(
                color: const Color(0xFF171D32),
                border: Border.all(color: const Color(0xFF00B0FF)),
                borderRadius: BorderRadius.circular(12),
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    'Selected Tier: ${telemetry.selectedTier.label}',
                    style: const TextStyle(
                      fontWeight: FontWeight.bold,
                      fontSize: 16,
                      color: Color(0xFF00B0FF),
                    ),
                  ),
                  const SizedBox(height: 6),
                  Text('Model ID: ${telemetry.selectedTier.modelId}'),
                  Text('Latency: ${telemetry.latencyMs} ms'),
                  Text('Reason: ${telemetry.routingReason}'),
                  const Divider(height: 24),
                  SelectableText(_lastResponse!.text),
                ],
              ),
            ),
          ],
        ],
      ),
    );
  }
}
Step 8 of 8 | Verification, Audit & Resources

Verification Checklist, Production Audit & Official Resources

1. End-to-End Verification Checklist

Create the automated test file test/smart_hybrid_ai_router_test.dart below to verify that strict PII stays on-device, privileged requests route to the Genkit Dart server, and cloud failures automatically fall back to LiteRT-LM:

1. Checklist de Vérification Finale

Créez le fichier de test automatisé test/smart_hybrid_ai_router_test.dart ci-dessous pour vérifier par les tests que les données PII strictes restent sur l'appareil, que les requêtes privilégiées sont dirigées vers le serveur Genkit Dart et que les pannes réseau déclenchent automatiquement le repli vers LiteRT-LM :

test/smart_hybrid_ai_router_test.dart | Automated Router Unit Tests DART
// test/smart_hybrid_ai_router_test.dart
import 'package:test/test.dart';
import 'package:hybrid_ai_router_codelab/application/smart_hybrid_ai_router.dart';
import 'package:hybrid_ai_router_codelab/data/firebase_ai_cloud_engine.dart';
import 'package:hybrid_ai_router_codelab/data/genkit_server_engine.dart';
import 'package:hybrid_ai_router_codelab/data/litert_edge_engine.dart';
import 'package:hybrid_ai_router_codelab/domain/ai_models.dart';

void main() {
  late SmartHybridAiRouter router;

  setUp(() {
    router = SmartHybridAiRouter(
      cloudEngine: FirebaseAiCloudEngine(),
      serverEngine: GenkitServerEngine(
        flowEndpoint: Uri.parse('https://api.example.com/verifyQuotaFlow'),
        idTokenProvider: () async => 'mock-firebase-id-token',
      ),
      edgeEngine: LiteRtEdgeEngine(),
    );
  });

  test('Routes strict PII prompts exclusively to on-device LiteRT-LM', () async {
    final res = await router.routeAndExecute(
      const RoutingContext(
        prompt: 'Summarize patient medical record #9482.',
        isOnline: true,
        sensitivity: TaskSensitivity.strictLocalPii,
      ),
    );
    expect(res.telemetry.selectedTier, equals(AiExecutionTier.edgeLiteRt));
    expect(res.telemetry.didFallbackToEdge, isFalse);
  });

  test('Routes privileged tool requests to Genkit Dart Server when online', () async {
    final res = await router.routeAndExecute(
      const RoutingContext(
        prompt: 'Upgrade organization subscription tier and issue invoice.',
        isOnline: true,
        requiresPrivilegedServerTool: true,
      ),
    );
    expect(res.telemetry.selectedTier, equals(AiExecutionTier.serverGenkitAgent));
  });

  test('Falls back to LiteRT-LM automatically when Cloud engine throws', () async {
    final failingRouter = SmartHybridAiRouter(
      cloudEngine: FirebaseAiCloudEngine(
        invoker: (_, _, _) async => throw Exception('Network socket closed'),
      ),
      serverEngine: GenkitServerEngine(
        flowEndpoint: Uri.parse('https://api.example.com/verifyQuotaFlow'),
        idTokenProvider: () async => 'mock-firebase-id-token',
      ),
      edgeEngine: LiteRtEdgeEngine(),
    );

    final res = await failingRouter.routeAndExecute(
      const RoutingContext(
        prompt: 'Analyze this detailed multi-paragraph architectural specification '
            'and compare all latency trade-offs across regions and mobile carriers '
            'with full token budget breakdowns for 10,000 daily active users.',
        isOnline: true,
      ),
    );

    expect(res.telemetry.selectedTier, equals(AiExecutionTier.edgeLiteRt));
    expect(res.telemetry.didFallbackToEdge, isTrue);
  });
}

Run static analysis and execute the unit test suite:

Lancez l'analyse statique et exécutez la suite de tests unitaires :

Terminal | Run Static Analysis & Unit Tests SHELL
flutter pub get
dart analyze
flutter test
flutter run

2. Key Engineering Takeaways

  1. Provision Before Coding: Always run npx firebase-tools@latest init ailogic before calling firebase_ai; flutterfire configure alone does not enable the backend proxy.
  2. Protect Client Quotas with App Check: Never ship firebase_ai to production without activating FirebaseAppCheck (Play Integrity on Android and App Attest on iOS).
  3. Enforce Server-Side Security Invariants: Any tool that mutates financial balances, subscription quotas, or security roles belongs exclusively inside a Genkit Dart Flow on a trusted server.
  4. Guarantee Continuity with LiteRT-LM Fallback: Wrapping cloud and server engines with an automatic fallback to a mutex-protected LiteRT-LM engine ensures your users never hit a dead screen when offline.

2. Points Clés à Retenir

  1. Provisionner avant de coder : Exécutez toujours npx firebase-tools@latest init ailogic avant d'appeler firebase_ai ; flutterfire configure seul n'active pas le proxy backend.
  2. Protéger les quotas clients avec App Check : Ne déployez jamais firebase_ai en production sans activer FirebaseAppCheck (Play Integrity sur Android et App Attest sur iOS).
  3. Appliquer les invariants de sécurité côté serveur : Tout outil modifiant des soldes financiers, des quotas d'abonnement ou des rôles de sécurité doit résider exclusivement dans un Flow Genkit Dart sur un serveur de confiance.
  4. Garantir la continuité grâce au repli LiteRT-LM : Encapsuler les moteurs Cloud et Serveur avec une bascule automatique vers un moteur LiteRT-LM protégé par mutex garantit une expérience sans interruption hors-ligne.
Codelab Complete | Hybrid AI Architecture Mastered

Built by Georges Byona (@glosings0n), Founder @ LosingTek & Founder of the FlutterFire Summer Camp.

Conçu par Georges Byona (@glosings0n), Fondateur @ LosingTek & Fondateur du FlutterFire Summer Camp.