Claude Cursor Skill

generate-images-with-firebase-ai

Use when generating or editing images from Flutter/Dart with Firebase AI Logic and a Gemini image model (Nano Banana), making the first call work, choosing Gemini Developer API vs Vertex AI, hitting quota, billing or App Check failures, getting empty or image-only responses, send

LLM Mart · 0 points · 12 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download evanca-flutter-ai-rules-skills_generate-images-with-firebase-ai-7d226d8.zip · 13 KB
Part of evanca/flutter-ai-rules — 37 skills

Install

skills CLI npx skills add https://github.com/evanca/flutter-ai-rules/tree/main/skills/generate-images-with-firebase-ai
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install evanca-flutter-ai-rules@llmmart
Git git clone https://github.com/evanca/flutter-ai-rules.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole evanca/flutter-ai-rules collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Generating images with Firebase AI Logic

Gemini image models return interleaved text and image parts from one call. The response is a sequence to walk, not a string to read.

A request that comes back empty is usually a configuration problem rather than a bug in your code, so section 1 covers the three settings that cause it.

1. Three things that block the very first call

A first call that returns an error, an empty response, or a 403 is almost always one of these rather than your Dart. Rule them out before you debug code.

Billing. Image generation has no free tier. On a Spark-plan project the image models return limit: 0 for generate_content_free_tier_requests, so the first request fails on quota having made zero requests. Text models do work on Spark, which means "my other Gemini call works" proves nothing. Upgrade to Blaze, then verify the current limits rather than trusting this note:

gcloud services quota list --service=generativelanguage.googleapis.com --consumer=projects/YOUR_PROJECT_ID

App Check. Firebase AI Logic enforces App Check when the project has it turned on. Otherwise the endpoint is open to anyone who extracts your config from the shipped client, and that config is public by design. Anything reachable from a device you do not control needs App Check. Debug builds attest with a debug provider, release builds with a real one. Web has a specific trap that costs an afternoon, described in references/setup.md.

responseModalities. Without it the model has no permission to return an image, so you get text describing the picture it would have drawn. Set both modalities, as in the call below.

2. The minimal call that works

final model = FirebaseAI.googleAI().generativeModel(
  model: 'gemini-3.1-flash-image',
  generationConfig: GenerationConfig(
    responseModalities: [
      ResponseModalities.text,
      ResponseModalities.image,
    ],
    imageConfig: ImageConfig(
      aspectRatio: ImageAspectRatio.landscape16x9,
      imageSize: ImageSize.size2K,
    ),
  ),
);

final response = await model.generateContent([
  Content.multi([
    TextPart(prompt),
    InlineDataPart('image/jpeg', selfieBytes), // omit for text-to-image
  ]),
]);

FirebaseAI.googleAI() is the Gemini Developer API. Prefer it: it needs no GCP surface of its own, and its free tier covers text. FirebaseAI.vertexAI() requires Blaze regardless of model and buys you GCP-side controls, so reach for it when the project already lives in Vertex rather than by default.

Do not pass appCheck: or auth: to googleAI(). Both parameters are deprecated in current firebase_ai; the instance resolves them from the FirebaseApp on its own.

For model IDs, aspect-ratio and size enums, and when Imagen beats Gemini, read references/models.md.

3. Reading the response

The convenience .text accessor does not capture the full sequence, so walk the parts directly. Pattern-match on the part type, which keeps the switch correct when the SDK adds new part types:

Uint8List? image;
final buffer = StringBuffer();
for (final part in candidate.content.parts) {
  switch (part) {
    case InlineDataPart(:final bytes):
      image ??= bytes;          // first image wins
    case TextPart(:final text):
      buffer.write(text);
    default:
      break;
  }
}

Keep the interpretation of a response in its own pure function taking a Candidate. Candidate, Content, TextPart and InlineDataPart are all publicly constructible, so that function is testable with real SDK types, no Firebase and no test doubles. It is the one seam in this stack that unit tests genuinely reach.

When no image comes back

An empty result surfaces as a blank error and reads like a client bug, which sends people debugging the wrong half of the system. The response carries the reason, so report it. In order:

Check Where Means
response.promptFeedback?.blockReason before candidates Your input was rejected. Read blockReasonMessage too.
response.candidates empty n/a Nothing generated at all.
candidate.finishReason on the candidate The model stopped: safety, recitation, or a token limit. finishMessage adds detail.
No image but text present after walking parts It answered in prose instead of drawing. Usually a prompt problem.
Everything empty, no reason n/a Say so plainly and let the user retry. This happens intermittently.

Image-only responses, with no text at all, also happen intermittently on prompts that reliably return both. If you ask for text alongside the image, treat its absence as normal and degrade instead of throwing.

4. Sending a user photo

Downscale before you send. InlineDataPart.toJson() base64-encodes the bytes synchronously on the main isolate, so a full-size phone photo freezes the UI for seconds while the request is built. Gemini downsamples large images anyway, so you pay for detail that is then discarded. Scale at pick time rather than after:

final file = await picker.pickImage(
  source: ImageSource.gallery,
  maxWidth: 1280,
  maxHeight: 1280,
  imageQuality: 85,
);

Size the cap to your subject rather than to a habit. 1280px on the long edge holds a face or a single figure comfortably, while fine texture, legible text in the source, or a wide scene the model has to read across will want more. Match mimeType to what the picker actually returned.

5. Sizing the result

Use ImageConfig when your ratio is one of the supported enum values, because it is a real constraint. Asking for a ratio in the prompt text is a suggestion the model frequently ignores. If you need a ratio the enum does not offer, 2:1 for instance, measure what came back and lay out from the measurement:

final descriptor = await ui.ImageDescriptor.encoded(
  await ui.ImmutableBuffer.fromUint8List(bytes),
);
final size = ui.Size(descriptor.width.toDouble(), descriptor.height.toDouble());
descriptor.dispose();

Wrap it so a failure returns null instead of throwing. An image you cannot measure is still an image worth showing.

6. Getting text and image from one call

You often want machine-readable data about the image the model just drew, such as a caption or the names it invented. Once that data is pixels your app cannot read it, so ask for it as text in the same call and reconcile the two halves in the prompt: "the title painted into the image must match the JSON character for character."

Parse that text defensively. Asked for a fenced ```json block, the model will across one session return fenced JSON, bare JSON, prose-wrapped JSON, and nothing at all. Try the fence, fall back to the first balanced {...}, and return null instead of throwing.

7. Prompting

The failures here are not code failures, and no unit test reaches them. The recurring ones have specific fixes worth knowing before you write the first prompt: placeholder words painted literally into the artwork, the source photo's clothing surviving an outfit change, text spelled differently in two places, and panels that do not share a background. Read references/prompting.md.

8. Deciding what to test

The seam is at your boundary. Unit tests protect your interpretation of a response and nothing past it, so a green suite says nothing about whether the app produces a good image. references/testing.md covers the layers and what each one cannot reach.

External documentation

Everything here is a summary that will drift. When a detail matters, confirm it at the source.

Reference files

  • references/setup.md: Firebase console path, provider choice, billing, and the web App Check debug-token trap
  • references/models.md: model IDs, aspect-ratio and size enums, Gemini vs Imagen
  • references/prompting.md: the mistakes image prompts actually make, and the phrasings that fix them
  • references/testing.md: unit, golden, e2e and eval layers, and what each cannot reach
Files (flutter-ai-rules)
  • references
    • models.md 3 KB
      # Models and output controls
      
      ## Model IDs
      
      Image model IDs churn faster than anything else here. Treat any ID written
      down, including these, as a starting guess to confirm rather than a fact.
      
      `gemini-3.1-flash-image` is Nano Banana 2, the current default for
      text-and-image in, text-and-image out. `gemini-2.5-flash-image` is the previous
      generation and resolves to `gemini-2.5-flash-preview-image`.
      
      Confirm what your project can actually reach before debugging a failing call,
      since a retired or misspelled ID and an unbilled project fail in similar ways:
      
      ```bash
      curl -s "https://generativelanguage.googleapis.com/v1beta/models?key=$GEMINI_API_KEY" | grep '"name"'
      ```
      
      ## Gemini image generation vs Imagen
      
      Both are reachable from `firebase_ai`. They are not interchangeable.
      
      | | Gemini (`generativeModel`) | Imagen (`imagenModel`) |
      | --- | --- | --- |
      | Input image | Yes: edit, restyle, keep a face | Text prompt only |
      | Text back with the image | Yes, interleaved parts | No |
      | Multiple images per call | No | `numberOfImages` |
      | Negative prompt | No | `negativePrompt` |
      | Watermark control | No | `addWatermark` |
      | Conversational refinement | Yes, via chat | No |
      
      Choose Gemini when the user supplies a photo, when you need machine-readable
      text alongside the image, or when you want to iterate on a result. Choose
      Imagen for pure text-to-image where you want several candidates in one call or
      a negative prompt.
      
      The rest of this file covers Gemini.
      
      ## `ImageConfig`, the only reliable size control
      
      ```dart
      GenerationConfig(
        responseModalities: [ResponseModalities.text, ResponseModalities.image],
        imageConfig: ImageConfig(
          aspectRatio: ImageAspectRatio.landscape16x9,
          imageSize: ImageSize.size2K,
        ),
      )
      ```
      
      Aspect ratios in `ImageAspectRatio`: `square1x1`, `portrait9x16`,
      `landscape16x9`, `portrait3x4`, `landscape4x3`, `portrait2x3`, `landscape3x2`,
      `portrait4x5`, `landscape5x4`, `portrait1x4`, `landscape4x1`, `portrait1x8`,
      `landscape8x1`, `ultrawide21x9`.
      
      Sizes in `ImageSize`: `size512`, `size1K`, `size2K`, `size4K`.
      
      Note what is missing. 2:1 is not on the list, and neither is any other ratio you
      might reasonably want. If yours is absent, asking for it in the prompt text is a
      suggestion the model frequently ignores, so measure the returned image and lay
      out from the measurement. Both fields are optional, and the enums grow between
      releases, so check the installed package rather than assuming this list is
      current:
      
      ```bash
      grep -A 3 "enum ImageAspectRatio" ~/.pub-cache/hosted/pub.dev/firebase_ai-*/lib/src/image_config.dart
      ```
      
      ## Cost
      
      Image generation is billed per generated image and is far more expensive than
      text. Two consequences are worth designing around from the start.
      
      A retry costs the same as the first try, so make failure states offer retry
      deliberately rather than looping automatically.
      
      An end-to-end test suite spends real money per run, roughly 30 to 60 seconds
      and a live billed generation per pass. That argues for running it occasionally
      against localhost rather than on every commit.
      
    • prompting.md 4.6 KB
      # Prompting image models
      
      None of these failures are code failures, and no unit test reaches any of them.
      Each one costs a generation to discover, and they recur across projects, so
      they are worth reading before you write the first prompt.
      
      ## The model reads your formatting as content
      
      Shouty capitals get painted into the image. A prompt containing `ARTIST NAME`
      produced artwork with the literal words "ARTIST NAME" set in display type. The
      model cannot tell your emphasis from text you want reproduced, and capitals
      read as lettering to typeset.
      
      Two fixes, and you want both. Never emphasise with capitals in a prompt that
      generates an image; use ordinary sentences. Then state the exclusion
      positively:
      
      > Every piece of text in the artwork must be one of the invented strings above.
      > Never paint instruction words, field labels or placeholders such as "artist
      > name" or "title" into the image.
      
      The same goes for any structural scaffolding, including bracketed
      placeholders, `<tags>` and `{{template}}` markers. If it looks like text, it
      can end up in the picture.
      
      ## Attributes of a source photo survive unless you actively negate them
      
      Asking for a new outfit gets you the old outfit in a new colour. Face, pose and
      framing carry over readily, which is usually what you want, but so do clothing,
      background and lighting, which usually is not.
      
      Negate the specific attributes rather than the category, and say why it
      matters:
      
      > Wardrobe, and this is important: completely replace the clothing worn in the
      > supplied photo. Do not keep the original garment, its colour, its pattern or
      > its neckline. Nothing the person is wearing in the source photo should
      > survive into the artwork.
      
      "Dress them differently" does not do this. Enumerating garment, colour, pattern
      and neckline does.
      
      ## Text appearing twice will be spelled two ways
      
      If the same string is typeset in two places, such as front and back panels or a
      title and a spine, the model will drift between them. It is generating pixels,
      not copying a variable.
      
      Say explicitly that the two are compared: "The album title must be spelled
      identically on both halves, because they are shown side by side." Naming the
      reason works better than repeating the instruction.
      
      ## Multi-panel layouts do not share a background by default
      
      Ask for two panels and you get two separate photographs butted together, with a
      visible seam and a tonal step at the join. Describe the continuity as the goal
      rather than the panels as the units:
      
      > The two halves must read as one continuous image. The background must flow
      > unbroken across the centre fold, with no seam, no tonal step and no visible
      > join.
      
      ## Let the model own the whole layout
      
      Compositing text over a deliberately empty region in Flutter is worse than
      asking the model to typeset it. The model lays type out better than a
      constraint-based layout can, and a region it typeset itself is a region whose
      background it composed around the type. Splitting the job is what produces the
      seam problem above.
      
      So the image is the output. Do not plan to parse anything back out of the
      pixels.
      
      ## Vary one axis, keep the rest fixed
      
      To make repeat runs feel different without feeling like a different product,
      fix everything structural in the prompt and rotate a single art-direction
      block.
      
      Move the visual attributes together, as named presets. Rotating palette alone
      puts chrome clothing on a grunge backdrop, which reads as a mistake rather than
      a look. Palette, wardrobe, typography and treatment all belong to one
      reference:
      
      ```dart
      class CoverStyle {
        final String name;        // surfaced in the UI so a good roll is recognisable
        final String palette;
        final String wardrobe;
        final String typography;
        final String treatment;
      }
      ```
      
      Log which preset produced each generation. When a result comes back unusually
      good or unusually bad, that log is the only record of what caused it.
      
      Close the art direction with a commitment line, such as "Commit fully to this
      art direction: the palette, wardrobe, lettering and film treatment above should
      all be unmistakable at a glance." A hedged interpretation of a strong reference
      is what produces bland output.
      
      ## Asking for structured text alongside the image
      
      Ask for a fenced ```json block, keep the schema to a handful of flat string
      keys, and reconcile it with the pixels explicitly: "The name painted into the
      artwork must match the JSON above character for character."
      
      Then parse it defensively. Across one session the same prompt returned fenced
      JSON, bare JSON, prose-wrapped JSON, and no text at all. Try the fence, fall
      back to the first balanced `{...}`, and return null instead of throwing. An
      image without its caption is still worth showing.
      
    • setup.md 5 KB
      # Setup: console, billing, App Check
      
      ## Console path
      
      Firebase console, create project, **Firebase AI Logic** in the sidebar, choose
      a provider, then follow the API-enablement checklist. The wizard hands off to
      the CLI, which needs the Flutter project to already exist:
      
      ```bash
      dart pub global activate flutterfire_cli
      ```
      
      ```bash
      flutterfire configure --project=YOUR_PROJECT_ID
      ```
      
      That registers the per-platform apps and writes `lib/firebase_options.dart`.
      
      ## Two toggles the wizard turns on for you
      
      Both default to on and are easy to accept without noticing. Neither is required
      by Firebase AI Logic, and neither is yours to decide. Surface them to whoever
      owns the project rather than picking silently.
      
      **Gemini in Firebase** is the console-side AI assistant. It is separate from
      Firebase AI Logic, so turning it off does not affect your image calls.
      Accepting it also accepts the GCP ToS and the Generative AI Service Specific
      Terms, which some organisations need to review first. Leave it on if the team
      wants the assistant.
      
      **Google Analytics** creates a linked Analytics property. Weigh it as a
      data-collection decision. It brings a privacy policy and consent obligations in
      some jurisdictions, and it feeds Crashlytics audiences and Remote Config
      targeting if the project uses them. Enabling it later means creating the
      property and re-running `flutterfire configure`.
      
      If the project has an existing convention for either, follow it. If not, ask
      before creating the project, because both are more annoying to change
      afterwards than to set correctly now.
      
      ## Provider choice
      
      | | Gemini Developer API (`FirebaseAI.googleAI()`) | Vertex AI (`FirebaseAI.vertexAI()`) |
      | --- | --- | --- |
      | Plan | Spark works for text | Blaze required, always |
      | Setup | Minimal | GCP project surface |
      | Image generation | Blaze required | Blaze required |
      | Pick it when | Default | Already on Vertex, or you need GCP-side controls |
      
      Neither provider gives you free image generation. The difference is only where
      text-model work is free.
      
      ## Billing
      
      Image generation has no free tier. On Spark the image models report `limit: 0`
      for `generate_content_free_tier_requests`, so the very first request fails on
      quota having made zero requests. That looks like a broken key or a wrong model
      ID, and it sends people editing code that is already correct.
      
      Upgrade to Blaze under console, settings, Usage and billing, Modify plan. Blaze
      is pay-as-you-go and the existing free tiers still apply where they exist, so
      services other than image generation cost what they cost today. Set a budget
      alert at the same time, since image generation is the one line item here with
      no free allowance and a runaway retry loop bills immediately.
      
      Check current limits directly rather than trusting any written note, because
      quota policy moves:
      
      ```bash
      gcloud services quota list --service=generativelanguage.googleapis.com --consumer=projects/YOUR_PROJECT_ID
      ```
      
      ## App Check
      
      Firebase AI Logic enforces App Check when the project has it enabled. Without
      it, anyone who pulls your Firebase config out of the shipped client can bill
      generations to your project. The config is public by design, so this is a real
      exposure the moment the app leaves your machine.
      
      Register each app under console, App Check, then activate in `main()` before
      the first AI call:
      
      ```dart
      await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
      await FirebaseAppCheck.instance.activate(
        providerWeb: kReleaseMode
            ? ReCaptchaV3Provider(_siteKey)
            : WebDebugProvider(debugToken: _debugToken.isEmpty ? null : _debugToken),
      );
      ```
      
      ### The web debug-token trap
      
      `WebDebugProvider()` with no token makes the SDK mint a fresh UUID per browser,
      and each one needs its own registration in the console. You register one, it
      works, then you open a different browser or a fresh profile and it fails again.
      That reads as flaky App Check rather than as a new token each time.
      
      Register one fixed debug token in the console and pass it to the constructor.
      Do not set `self.FIREBASE_APPCHECK_DEBUG_TOKEN` yourself beforehand: the plugin
      sets that global while resolving the provider, and overwrites whatever is there
      with `true` when no token was supplied, silently undoing the value you just
      pinned.
      
      ### Which keys may live in source
      
      The reCAPTCHA v3 **site** key is public by design. It ships in the client and
      is visible to anyone who loads the page, so keeping it in source is fine. The
      matching **secret** key lives only in the console and must never appear in the
      repo.
      
      The App Check debug token is a bypass credential. Keep it out of source and
      supply it per environment:
      
      ```bash
      flutter run --dart-define=APPCHECK_DEBUG_TOKEN=your-token
      ```
      
      Read both with `String.fromEnvironment`, giving the site key a default and the
      debug token none.
      
      ### Before shipping
      
      `ReCaptchaV3Provider` never executes in debug, which makes the release path the
      most likely thing to break in production and the least reachable by any test.
      Add `localhost` to the reCAPTCHA key's domain list and run a release build
      locally once. That exercises the real provider end to end before you deploy.
      
    • testing.md 4.9 KB
      # What to test, and what testing cannot reach
      
      The seam is at your boundary. Everything past it needs evals rather than tests:
      whether the model understood the prompt, whether the image is any good, whether
      the type is legible.
      
      A green suite says nothing about whether the app produces a good image. It says
      your interpretation of a response is correct. That is worth having, and it is
      not where the bugs will be. Plan the other layers accordingly.
      
      ## Layer 1: unit tests on your interpretation of a response
      
      Cheap, fast, deterministic. Two functions are worth testing and they are the
      only two: parsing the model's structured text, and turning one `Candidate` into
      your draft type.
      
      `Candidate`, `Content`, `TextPart` and `InlineDataPart` are all publicly
      constructible, so these run against real SDK types with no Firebase and no test
      doubles. Nothing is verified against a stand-in.
      
      Cover the cases that actually occur: interleaved parts split correctly;
      image-only yields a draft with missing text rather than an error; a stopped
      generation reports its finish reason; a wholly empty response says so instead
      of trailing off; the first image wins when several arrive. For the text parser,
      fenced JSON, bare JSON, prose-wrapped JSON and no JSON at all must each end in
      usable data or a null, never an exception.
      
      `Candidate`'s constructor is positional, so build it through one helper in the
      test file. A signature change in `firebase_ai` then breaks one line rather than
      every test. That is also the reason to keep these tests few. The alternative to
      a brittle test here is no test at all, but there is no reason to multiply the
      brittleness.
      
      ## Layer 2: golden tests on layout, colour and geometry
      
      Deterministic without any font work. The test environment substitutes a fixed
      test font, so text renders as placeholder glyph boxes but renders identically
      every run. Geometry and colour come through intact, which is what most visual
      bugs actually are: a caption overlapping text the model printed into the same
      corner, an uploaded photo cropped instead of fitted, a button spanning the
      wrong column, an element clipped by a `Stack`.
      
      Goldens protect decisions already made. They will not tell you the first render
      is wrong.
      
      ## Layer 3: end-to-end on localhost, the wiring a user walks through
      
      [Patrol](https://patrol.leancode.co/) is a third-party Flutter UI-testing
      framework by LeanCode ([pub.dev](https://pub.dev/packages/patrol),
      [GitHub](https://github.com/leancodepl/patrol)). Evaluate it as a dependency on
      its own merits before adopting it. It launches Chromium through Playwright,
      which reaches the three things that otherwise need a human at the keyboard.
      Method names below come from its
      [web testing docs](https://patrol.leancode.co/documentation/web); check that
      page for current signatures rather than trusting this table.
      
      | Gap | Patrol web API |
      | --- | --- |
      | Native file dialog, so every upload needs a person | `uploadFile(files: [UploadFileData(...)])` |
      | Camera permission prompt cannot be accepted | `grantPermissions(permissions: [...])` |
      | Download button never verified end to end | `verifyFileDownloads()` |
      
      This layer costs real money per run. Each pass triggers a live billed
      generation and takes 30 to 60 seconds, which argues for running it occasionally
      rather than on every commit. It also verifies the flow, not the artwork. "An
      image came back, the fields populated, a file downloaded" is assertable; "the
      image is good" is not.
      
      `grantPermissions` handles the camera prompt. Producing actual frames needs
      Chromium's fake-media-device flags.
      
      If Patrol is not a dependency you want, the layer itself still matters. Any
      driver that can reach native file dialogs and browser permissions buys the same
      thing. What you lose without one is coverage of upload, permissions and
      download, which are exactly the steps a unit test cannot walk.
      
      ## Layer 4: evals on prompt adherence and image quality
      
      Sampled generations scored by a judge model. Nondeterministic, costs money per
      run, and the only layer that reaches the failures in `prompting.md`. Nothing
      else touches them.
      
      ## What sits outside all four
      
      Initialisation order. App Check minting a fresh token per browser is a web-only
      plugin behaviour that resolves to a stub off-web, so it is not reproducible in a
      VM test at all. Found by inspecting a live global in a running browser.
      
      Progress reporting. The API gives no progress signal, so any staged "working..."
      UI is paced on a timer. A test can assert the timer advances; it cannot assert
      the story it tells is true. Stop at the last stage rather than claiming a
      completion the model has not reported.
      
      Main-isolate stalls. A full-size photo freezing the UI during base64 encoding
      passes every correctness assertion. It needs a profile, or someone noticing.
      
      The release attestation path. `ReCaptchaV3Provider` never executes in debug,
      which makes it the most likely thing to break in production and the least
      reachable by a test. See the release check in `setup.md`.
      
  • SKILL.md 9 KB
    ---
    name: generate-images-with-firebase-ai
    description: "Use when generating or editing images from Flutter/Dart with Firebase AI Logic and a Gemini image model (Nano Banana), making the first call work, choosing Gemini Developer API vs Vertex AI, hitting quota, billing or App Check failures, getting empty or image-only responses, sending a user photo as input, controlling aspect ratio or size, writing the image prompt, or deciding what to test."
    license: MIT
    ---
    
    # Generating images with Firebase AI Logic
    
    Gemini image models return interleaved text and image parts from one call. The
    response is a sequence to walk, not a string to read.
    
    A request that comes back empty is usually a configuration problem rather than
    a bug in your code, so section 1 covers the three settings that cause it.
    
    ## 1. Three things that block the very first call
    
    A first call that returns an error, an empty response, or a 403 is almost
    always one of these rather than your Dart. Rule them out before you debug code.
    
    **Billing.** Image generation has no free tier. On a Spark-plan project the
    image models return `limit: 0` for `generate_content_free_tier_requests`, so
    the first request fails on quota having made zero requests. Text models do work
    on Spark, which means "my other Gemini call works" proves nothing. Upgrade to
    Blaze, then verify the current limits rather than trusting this note:
    
    ```bash
    gcloud services quota list --service=generativelanguage.googleapis.com --consumer=projects/YOUR_PROJECT_ID
    ```
    
    **App Check.** Firebase AI Logic enforces App Check when the project has it
    turned on. Otherwise the endpoint is open to anyone who extracts your config
    from the shipped client, and that config is public by design. Anything
    reachable from a device you do not control needs App Check. Debug builds
    attest with a debug provider, release builds with a real one. Web has a
    specific trap that costs an afternoon, described in `references/setup.md`.
    
    **`responseModalities`.** Without it the model has no permission to return an
    image, so you get text describing the picture it would have drawn. Set both
    modalities, as in the call below.
    
    ## 2. The minimal call that works
    
    ```dart
    final model = FirebaseAI.googleAI().generativeModel(
      model: 'gemini-3.1-flash-image',
      generationConfig: GenerationConfig(
        responseModalities: [
          ResponseModalities.text,
          ResponseModalities.image,
        ],
        imageConfig: ImageConfig(
          aspectRatio: ImageAspectRatio.landscape16x9,
          imageSize: ImageSize.size2K,
        ),
      ),
    );
    
    final response = await model.generateContent([
      Content.multi([
        TextPart(prompt),
        InlineDataPart('image/jpeg', selfieBytes), // omit for text-to-image
      ]),
    ]);
    ```
    
    `FirebaseAI.googleAI()` is the Gemini Developer API. Prefer it: it needs no GCP
    surface of its own, and its free tier covers text. `FirebaseAI.vertexAI()`
    requires Blaze regardless of model and buys you GCP-side controls, so reach for
    it when the project already lives in Vertex rather than by default.
    
    Do not pass `appCheck:` or `auth:` to `googleAI()`. Both parameters are
    deprecated in current `firebase_ai`; the instance resolves them from the
    `FirebaseApp` on its own.
    
    For model IDs, aspect-ratio and size enums, and when Imagen beats Gemini, read
    `references/models.md`.
    
    ## 3. Reading the response
    
    The convenience `.text` accessor does not capture the full sequence, so walk
    the parts directly. Pattern-match on the part type, which keeps the switch
    correct when the SDK adds new part types:
    
    ```dart
    Uint8List? image;
    final buffer = StringBuffer();
    for (final part in candidate.content.parts) {
      switch (part) {
        case InlineDataPart(:final bytes):
          image ??= bytes;          // first image wins
        case TextPart(:final text):
          buffer.write(text);
        default:
          break;
      }
    }
    ```
    
    Keep the interpretation of a response in its own pure function taking a
    `Candidate`. `Candidate`, `Content`, `TextPart` and `InlineDataPart` are all
    publicly constructible, so that function is testable with real SDK types, no
    Firebase and no test doubles. It is the one seam in this stack that unit tests
    genuinely reach.
    
    ### When no image comes back
    
    An empty result surfaces as a blank error and reads like a client bug, which
    sends people debugging the wrong half of the system. The response carries the
    reason, so report it. In order:
    
    | Check | Where | Means |
    | --- | --- | --- |
    | `response.promptFeedback?.blockReason` | before candidates | Your input was rejected. Read `blockReasonMessage` too. |
    | `response.candidates` empty | n/a | Nothing generated at all. |
    | `candidate.finishReason` | on the candidate | The model stopped: safety, recitation, or a token limit. `finishMessage` adds detail. |
    | No image but text present | after walking parts | It answered in prose instead of drawing. Usually a prompt problem. |
    | Everything empty, no reason | n/a | Say so plainly and let the user retry. This happens intermittently. |
    
    Image-only responses, with no text at all, also happen intermittently on
    prompts that reliably return both. If you ask for text alongside the image,
    treat its absence as normal and degrade instead of throwing.
    
    ## 4. Sending a user photo
    
    Downscale before you send. `InlineDataPart.toJson()` base64-encodes the bytes
    synchronously on the main isolate, so a full-size phone photo freezes the UI
    for seconds while the request is built. Gemini downsamples large images anyway,
    so you pay for detail that is then discarded. Scale at pick time rather than
    after:
    
    ```dart
    final file = await picker.pickImage(
      source: ImageSource.gallery,
      maxWidth: 1280,
      maxHeight: 1280,
      imageQuality: 85,
    );
    ```
    
    Size the cap to your subject rather than to a habit. 1280px on the long edge
    holds a face or a single figure comfortably, while fine texture, legible text
    in the source, or a wide scene the model has to read across will want more.
    Match `mimeType` to what the picker actually returned.
    
    ## 5. Sizing the result
    
    Use `ImageConfig` when your ratio is one of the supported enum values, because
    it is a real constraint. Asking for a ratio in the prompt text is a suggestion
    the model frequently ignores. If you need a ratio the enum does not offer, 2:1
    for instance, measure what came back and lay out from the measurement:
    
    ```dart
    final descriptor = await ui.ImageDescriptor.encoded(
      await ui.ImmutableBuffer.fromUint8List(bytes),
    );
    final size = ui.Size(descriptor.width.toDouble(), descriptor.height.toDouble());
    descriptor.dispose();
    ```
    
    Wrap it so a failure returns null instead of throwing. An image you cannot
    measure is still an image worth showing.
    
    ## 6. Getting text and image from one call
    
    You often want machine-readable data about the image the model just drew, such
    as a caption or the names it invented. Once that data is pixels your app cannot
    read it, so ask for it as text in the same call and reconcile the two halves in
    the prompt: "the title painted into the image must match the JSON character for
    character."
    
    Parse that text defensively. Asked for a fenced ```json block, the model will
    across one session return fenced JSON, bare JSON, prose-wrapped JSON, and
    nothing at all. Try the fence, fall back to the first balanced `{...}`, and
    return null instead of throwing.
    
    ## 7. Prompting
    
    The failures here are not code failures, and no unit test reaches them. The
    recurring ones have specific fixes worth knowing before you write the first
    prompt: placeholder words painted literally into the artwork, the source
    photo's clothing surviving an outfit change, text spelled differently in two
    places, and panels that do not share a background. Read
    `references/prompting.md`.
    
    ## 8. Deciding what to test
    
    The seam is at your boundary. Unit tests protect your interpretation of a
    response and nothing past it, so a green suite says nothing about whether the
    app produces a good image. `references/testing.md` covers the layers and what
    each one cannot reach.
    
    ## External documentation
    
    Everything here is a summary that will drift. When a detail matters, confirm it
    at the source.
    
    - [Firebase AI Logic docs](https://firebase.google.com/docs/ai-logic), including
      [get started](https://firebase.google.com/docs/ai-logic/get-started) and
      [generate images with Gemini](https://firebase.google.com/docs/ai-logic/generate-images-gemini)
    - [`firebase_ai`](https://pub.dev/packages/firebase_ai), the Dart SDK. Its
      source is the fastest way to settle an API question:
      `~/.pub-cache/hosted/pub.dev/firebase_ai-*/lib/src/`
    - [`image_picker`](https://pub.dev/packages/image_picker), which supplies the
      `pickImage` call in section 4. A third-party choice you can swap.
    - [Patrol](https://patrol.leancode.co/), a third-party E2E framework by
      LeanCode, discussed in `references/testing.md`
    
    ## Reference files
    
    - `references/setup.md`: Firebase console path, provider choice, billing, and the web App Check debug-token trap
    - `references/models.md`: model IDs, aspect-ratio and size enums, Gemini vs Imagen
    - `references/prompting.md`: the mistakes image prompts actually make, and the phrasings that fix them
    - `references/testing.md`: unit, golden, e2e and eval layers, and what each cannot reach
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related