Skip to content

Google Generative AI plugin

The examples on this page use these imports:

import (
"context"
"errors"
"log"
"github.com/firebase/genkit/go/ai"
"github.com/firebase/genkit/go/core/status"
"github.com/firebase/genkit/go/genkit"
"github.com/firebase/genkit/go/plugins/googlegenai"
"google.golang.org/genai"
)

google.golang.org/genai is the Google GenAI Go SDK. It is a separate module that supplies the config types this plugin takes, so go mod tidy will add it to your go.mod alongside Genkit.

The Google Generative AI plugin provides interfaces to Google’s Gemini models through the Gemini API.

To use this plugin, import the googlegenai package and pass googlegenai.GoogleAI to WithPlugins() in the Genkit initializer:

import "github.com/firebase/genkit/go/plugins/googlegenai"
g := genkit.Init(context.Background(), genkit.WithPlugins(&googlegenai.GoogleAI{}))

The plugin requires an API key for the Gemini API, which you can get from Google AI Studio.

Configure the plugin to use your API key by doing one of the following:

  • Set the GEMINI_API_KEY environment variable to your API key. If it is empty, GOOGLE_API_KEY is consulted next.

  • Specify the API key when you initialize the plugin:

genkit.WithPlugins(&googlegenai.GoogleAI{APIKey: "YOUR_API_KEY"})

However, don’t embed your API key directly in code! Use this feature only in conjunction with a service like Cloud Secret Manager or similar.

googlegenai.GoogleAI carries the whole configuration surface of the plugin:

FieldTypeDescription
APIKeystringAPI key to access the service. If empty, GEMINI_API_KEY then GOOGLE_API_KEY are consulted.
APIVersionstring"v1", "v1beta", or "v1alpha". If empty, the genai SDK default (v1beta) is used. Overridable per request through config.HTTPOptions.APIVersion.
BaseURLstringOverrides the default endpoint (https://generativelanguage.googleapis.com), for example to point at a proxy or an API gateway.
Headershttp.HeaderExtra HTTP headers sent with every request. They are merged over the plugin’s defaults, so a header set here wins on collision.
HTTPClient*http.ClientUsed verbatim when set; the default is http.DefaultClient. The plugin adds no instrumentation of its own, so wrap the transport with otelhttp.NewTransport to trace the provider’s HTTP calls.
Modelsmap[string]ai.ModelOptionsCorrects or extends what the plugin knows about a model, keyed by model ID. See Describing a model or embedder.
Embeddersmap[string]ai.EmbedderOptionsThe same, for embedders.
g := genkit.Init(context.Background(), genkit.WithPlugins(&googlegenai.GoogleAI{
APIKey: "YOUR_API_KEY",
APIVersion: "v1alpha",
BaseURL: "https://my-gateway.example.com",
Headers: http.Header{"X-Team": {"platform"}},
HTTPClient: myClient,
}))

The plugin registers no models at initialization. A model ID resolves when a request names it, so any ID the Gemini API serves works, including a model released after this version of the plugin and aliases such as gemini-flash-latest. The list below is the set of IDs the plugin curates capabilities for (label, supported inputs and outputs, config schema), not the set you can choose from. An uncurated ID resolves with the default capabilities for its kind.

Text and multimodal

  • gemini-2.5-flash
  • gemini-2.5-flash-lite
  • gemini-2.5-pro
  • gemini-omni-flash
  • gemini-3-flash-preview
  • gemini-3.8-flash
  • gemini-3.7-flash
  • gemini-3.6-flash
  • gemini-3.5-flash
  • gemini-3.5-flash-lite
  • gemini-3.1-pro-preview
  • gemini-3.1-flash-lite

Image output

  • gemini-2.5-flash-image
  • gemini-3.1-flash-image
  • gemini-3.1-flash-lite-image
  • gemini-3-pro-image

Image generation (Imagen)

  • imagen-4.0-fast-generate-001
  • imagen-4.0-generate-001
  • imagen-4.0-ultra-generate-001

Speech (TTS)

  • gemini-2.5-flash-preview-tts
  • gemini-2.5-pro-preview-tts
  • gemini-3.1-flash-tts-preview

Video (Veo)

  • veo-3.1-generate-preview
  • veo-3.1-fast-generate-preview
  • veo-3.1-lite-generate-preview

Name the model on the request:

resp, err := genkit.Generate(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("Tell me a joke."),
)
if err != nil {
return err
}
log.Println(resp.Text())

The basic sample is a runnable version of this, including the streaming form.

googlegenai.ModelRef pairs a model name with its typed configuration, so one value carries both:

model := googlegenai.ModelRef("googleai/gemini-flash-latest", &genai.GenerateContentConfig{
Temperature: genai.Ptr[float32](0.5),
MaxOutputTokens: 500,
})
resp, err := genkit.Generate(ctx, g, ai.WithModel(model), ai.WithPrompt("Tell me a joke."))
if err != nil {
return err
}
log.Println(resp.Text())

The config type follows the modality, so image and video models get their own constructors:

image := googlegenai.ImageModelRef("googleai/imagen-4.0-generate-001", &genai.GenerateImagesConfig{
AspectRatio: "1:1",
})
video := googlegenai.VideoModelRef("googleai/veo-3.1-generate-preview", &genai.GenerateVideosConfig{
AspectRatio: "16:9",
})

The plugin advertises this config as the request’s input schema and validates it on every call. Every field of *genai.GenerateContentConfig is valid config, so a Gemini API feature the Go SDK models is reachable from Genkit whether or not this page names it. The basic-media sample reads, edits, generates, and animates a picture in one program.

See Generating content with AI models for more information.

ThinkingConfig turns on Gemini’s internal reasoning. ThinkingBudget caps the tokens spent on it, ThinkingLevel picks a preset instead, and IncludeThoughts asks for thought summaries. Summaries come back as reasoning parts, readable through resp.Reasoning(), and the tokens they cost are reported in resp.Usage.ThoughtsTokens.

resp, err := genkit.Generate(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithConfig(&genai.GenerateContentConfig{
ThinkingConfig: &genai.ThinkingConfig{
IncludeThoughts: true,
ThinkingBudget: genai.Ptr[int32](8192),
},
}),
ai.WithPrompt("Which is heavier, a kilo of steel or a kilo of feathers?"),
)
if err != nil {
return err
}
log.Println(resp.Reasoning())
log.Println(resp.Text())

Gemini 3 and later take ThinkingLevel instead of a token budget: genai.ThinkingLevelMinimal, ThinkingLevelLow, ThinkingLevelMedium, or ThinkingLevelHigh.

resp, err := genkit.Generate(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithConfig(&genai.GenerateContentConfig{
SafetySettings: []*genai.SafetySetting{
{
Category: genai.HarmCategoryHateSpeech,
Threshold: genai.HarmBlockThresholdBlockMediumAndAbove,
},
{
Category: genai.HarmCategoryDangerousContent,
Threshold: genai.HarmBlockThresholdBlockOnlyHigh,
},
},
}),
ai.WithPrompt("Tell me a joke."),
)
if err != nil {
return err
}
log.Println(resp.Text())

Content the filter stops comes back as a response, not an error. See Blocked responses for how to detect it and where the raw ratings land.

Gemini’s server-side tools ride in the config’s Tools field, separately from Genkit tools, which you pass with ai.WithTools. Google Search grounding, Maps grounding, and URL context are all tools in that sense:

resp, err := genkit.Generate(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithConfig(&genai.GenerateContentConfig{
Tools: []*genai.Tool{
{GoogleSearch: &genai.GoogleSearch{}},
{URLContext: &genai.URLContext{}},
},
}),
ai.WithPrompt("What are the top tech news stories this week?"),
)
if err != nil {
return err
}
log.Println(resp.Text())
custom, _ := resp.Custom.(map[string]any)
candidates, _ := custom["candidates"].([]*genai.Candidate)
for _, cand := range candidates {
if cand.GroundingMetadata == nil {
continue
}
for _, chunk := range cand.GroundingMetadata.GroundingChunks {
if chunk.Web != nil {
log.Printf("source: %s (%s)", chunk.Web.Title, chunk.Web.URI)
}
}
}

Genkit does not model grounding metadata, so it arrives raw. resp.Custom is a map[string]any whose "candidates" key holds the []*genai.Candidate the service returned, and GroundingMetadata on each candidate carries the search queries, the chunks, and the per-segment supports. {GoogleMaps: &genai.GoogleMaps{}} is the Maps equivalent.

{CodeExecution: &genai.ToolCodeExecution{}} lets the model write and run code as part of its answer. The code it wrote and the result of running it come back as custom parts, which the plugin gives you accessors for:

resp, err := genkit.Generate(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithConfig(&genai.GenerateContentConfig{
Tools: []*genai.Tool{{CodeExecution: &genai.ToolCodeExecution{}}},
}),
ai.WithPrompt("Calculate the 20th Fibonacci number."),
)
if err != nil {
return err
}
if code := googlegenai.GetExecutableCode(resp.Message); code != nil {
log.Printf("%s:\n%s", code.Language, code.Code)
}
if result := googlegenai.GetCodeExecutionResult(resp.Message); result != nil {
log.Printf("outcome %s: %s", result.Outcome, result.Output)
}

GetExecutableCode and GetCodeExecutionResult return the first match in a message, or nil. googlegenai.ToExecutableCode and googlegenai.ToCodeExecutionResult do the same for a single *ai.Part when you need to walk the content yourself.

Gemini 2.5 and later cache repeated prompt prefixes on their own: it is on by default, there is no storage charge, and a hit shows up as resp.Usage.CachedContentTokens. Explicit caching is for when the hit has to be guaranteed. (*ai.Message).WithCacheTTL marks a message as the end of the cached prefix: everything up to and including it is uploaded to a cache resource once, and later requests reference the resource instead of resending the content, so the wire carries only the messages after the marker.

resp, err := genkit.Generate(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithMessages(
ai.NewUserMessage(ai.NewMediaPart("application/pdf", pdfDataURL)).WithCacheTTL(3600),
ai.NewUserTextMessage("Summarize the document."),
),
)
if err != nil {
return err
}
log.Println(resp.Usage.CachedContentTokens)

The TTL is in whole seconds and must be positive, and the resource is billed for that lifetime. The marker is inclusive, so keep the question in a separate message. A request that marks its last message has nothing left to send inline, and carries a minimal turn instead of failing.

A later request reuses the cache rather than paying to build it again. Replaying resp.History() carries the cache’s name back on the marked message’s metadata, and rebuilding the same prefix by hand finds it too: the plugin names every cache by a hash of its contents and the model, and looks for a match before creating one. A cache that has expired or no longer matches is rebuilt rather than reported as an error. (*ai.Message).WithCacheName points a request at a cache whose name you kept yourself; if that cache is gone, the request still goes through, uncached.

next, err := genkit.Generate(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithMessages(resp.History()...),
ai.WithPrompt("Now list the action items."),
)

Two request shapes are refused before the request goes out, both with INVALID_ARGUMENT: a request that also carries tools, and one that carries a system message.

Image, video, and audio output arrive as media parts, not as text. Inline bytes are wrapped in a data: URL and served-file output carries the file URI, so the same accessor works for both:

resp, err := genkit.Generate(ctx, g,
ai.WithModelName("googleai/gemini-2.5-flash-image"),
ai.WithPrompt("A serene Japanese garden with cherry blossoms."),
)
if err != nil {
return err
}
for _, part := range resp.MediaParts() {
log.Printf("%s: %s", part.ContentType, part.Text)
}

On a media part, part.Text holds the URL. For a data: URL, split on the comma and base64-decode the tail to get the bytes. resp.Media() returns the first URL alone when one is all you need.

Embedders resolve on demand exactly as models do. These are the IDs the plugin curates:

Embedder IDDimensionsInput
gemini-embedding-23072text, image, video
gemini-embedding-0013072text
text-embedding-005768text
text-embedding-004768text
text-multilingual-embedding-002768text
multimodalembedding768text, image, video

Any other ID resolves at 768 dimensions with text input.

resp, err := genkit.Embed(ctx, g,
ai.WithEmbedderName("googleai/gemini-embedding-001"),
ai.WithTextDocs(userInput),
)
if err != nil {
return err
}

googlegenai.EmbedderRef pairs an embedder with its typed config the same way ModelRef does:

embedder := googlegenai.EmbedderRef("googleai/gemini-embedding-001", &genai.EmbedContentConfig{
TaskType: "RETRIEVAL_DOCUMENT",
})
resp, err := genkit.Embed(ctx, g,
ai.WithEmbedder(embedder),
ai.WithTextDocs("Machine learning models process data to make predictions."),
)
if err != nil {
return err
}
log.Println(resp.Embeddings[0].Embedding)

EmbedderRef returns an ai.EmbedderRef, not an ai.Embedder, so hold it in a field or variable typed ai.EmbedderRef. ai.WithEmbedder accepts either type. Passing ai.WithConfig as well overrides the config the ref carries.

Requests are split into batches of 100 documents, so an embed call with more documents than the service accepts in one request still works. The response carries one embedding per input, in input order.

Batching is the only limit Genkit handles for you. Each individual document still has to fit the model’s own input limit, 2,048 tokens for gemini-embedding-001. Genkit neither truncates nor splits a document, so chunk long text before embedding it; an oversized document fails at the service. Check the current limit on the embeddings model card.

See Retrieval-augmented generation (RAG) for more information.

The Models and Embedders maps correct or extend what the plugin knows about an ID. Use them to describe a model the plugin has never heard of, or to pin a capability the plugin resolves wrongly:

g := genkit.Init(context.Background(), genkit.WithPlugins(&googlegenai.GoogleAI{
Models: map[string]ai.ModelOptions{
"gemini-flash-latest": {
Supports: &ai.ModelSupports{
Multiturn: true,
Tools: true,
SystemRole: true,
Media: true,
},
},
},
Embedders: map[string]ai.EmbedderOptions{
"gemini-embedding-001": {Dimensions: 1536},
},
}))

Entries overlay rather than replace: a field left at its zero value keeps what the plugin resolves, so an entry can pin one capability without restating the label or the config schema. Keys may be bare ("gemini-flash-latest") or provider-prefixed ("googleai/gemini-flash-latest"), and Gemini, Imagen, Veo, and embedder IDs are all keyed the same way. One entry reaches both the listing the Dev UI shows and the action built to serve a request.

DeprecatedUse instead
(*GoogleAI).DefineModelthe Models map
(*GoogleAI).DefineEmbedderthe Embedders map
(*GoogleAI).IsDefinedEmbedderdrop the call
googlegenai.GoogleAIModelgenkit.LookupModel
googlegenai.GoogleAIEmbeddergenkit.LookupEmbedder
googlegenai.GoogleAIModelRefgooglegenai.ModelRef with the provider-prefixed name

DefineModel and DefineEmbedder build a value and hand it back without registering it, so the capabilities you passed never reach the code that serves the request: generation resolves a model from the name alone. A map entry reaches both paths, which is why it is the only form that takes effect.

Content stopped by a safety filter comes back as a response, not an error:

resp, err := genkit.Generate(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("Tell me a joke."),
)
if err != nil {
return err
}
if resp.FinishReason == ai.FinishReasonBlocked {
log.Printf("response blocked: %s", resp.FinishMessage)
// Raw safety data: resp.Custom["candidates"] holds []*genai.Candidate and
// resp.Custom["promptFeedback"] holds the prompt-level feedback.
return nil
}

FinishMessage carries the service’s explanation. resp.Custom is a map[string]any: the raw ratings are attached under its "candidates" key as []*genai.Candidate and, when the prompt itself was blocked, under "promptFeedback" as a *genai.GenerateContentResponsePromptFeedback.

The typed helpers report a refusal as an error instead. genkit.GenerateData, genkit.GenerateDataStream, and the DataPrompt execute methods return ai.ErrGenerationBlocked, carrying FinishMessage and with the response beside the error, because the value they promise cannot be produced. Match it with errors.Is.

Errors from every action carry the status the service reported, so status-aware middleware such as middleware.Retry and middleware.Fallback can classify them. When the service asks for a specific backoff, googlegenai.RetryDelay reads it:

_, err := genkit.Generate(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("Tell me a joke."),
)
if err != nil {
if errors.Is(err, status.ErrResourceExhausted) {
if delay, ok := googlegenai.RetryDelay(err); ok {
// Hand the delay to your retry policy instead of guessing a backoff.
time.Sleep(delay)
}
}
return err
}

The second result is false when the error carries no retry information. The same value also rides on the status error’s details under the key retryAfterMs, in milliseconds. See Error types and the basic-errors sample for how a classified error travels to the HTTP boundary.

Client() returns the *genai.Client the plugin authenticated, which is how you reach service features Genkit does not wrap: Files, Caches, Batches, and Tunings.

plugin := &googlegenai.GoogleAI{}
g := genkit.Init(ctx, genkit.WithPlugins(plugin))
client, err := plugin.Client()
if err != nil {
return err
}
file, err := client.Files.UploadFromPath(ctx, "photo.jpg", &genai.UploadFileConfig{
MIMEType: "image/jpeg",
})
if err != nil {
return err
}
resp, err := genkit.Generate(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPromptParts(
ai.NewTextPart("Describe this picture."),
ai.NewMediaPart("image/jpeg", file.URI),
),
)

Call it after genkit.Init. Before that, it returns a FAILED_PRECONDITION error and a nil client. The basic-media sample uses this to upload a picture before describing it.