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.
Configuration
Section titled “Configuration”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_KEYenvironment variable to your API key. If it is empty,GOOGLE_API_KEYis 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.
Plugin options
Section titled “Plugin options”googlegenai.GoogleAI carries the whole configuration surface of the plugin:
| Field | Type | Description |
|---|---|---|
APIKey | string | API key to access the service. If empty, GEMINI_API_KEY then GOOGLE_API_KEY are consulted. |
APIVersion | string | "v1", "v1beta", or "v1alpha". If empty, the genai SDK default (v1beta) is used. Overridable per request through config.HTTPOptions.APIVersion. |
BaseURL | string | Overrides the default endpoint (https://generativelanguage.googleapis.com), for example to point at a proxy or an API gateway. |
Headers | http.Header | Extra HTTP headers sent with every request. They are merged over the plugin’s defaults, so a header set here wins on collision. |
HTTPClient | *http.Client | Used 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. |
Models | map[string]ai.ModelOptions | Corrects or extends what the plugin knows about a model, keyed by model ID. See Describing a model or embedder. |
Embedders | map[string]ai.EmbedderOptions | The 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,}))Generative models
Section titled “Generative models”Model IDs
Section titled “Model IDs”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-flashgemini-2.5-flash-litegemini-2.5-progemini-omni-flashgemini-3-flash-previewgemini-3.8-flashgemini-3.7-flashgemini-3.6-flashgemini-3.5-flashgemini-3.5-flash-litegemini-3.1-pro-previewgemini-3.1-flash-lite
Image output
gemini-2.5-flash-imagegemini-3.1-flash-imagegemini-3.1-flash-lite-imagegemini-3-pro-image
Image generation (Imagen)
imagen-4.0-fast-generate-001imagen-4.0-generate-001imagen-4.0-ultra-generate-001
Speech (TTS)
gemini-2.5-flash-preview-ttsgemini-2.5-pro-preview-ttsgemini-3.1-flash-tts-preview
Video (Veo)
veo-3.1-generate-previewveo-3.1-fast-generate-previewveo-3.1-lite-generate-preview
Generating content
Section titled “Generating content”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.
Model references
Section titled “Model references”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.
Thinking
Section titled “Thinking”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.
Safety settings
Section titled “Safety settings”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.
Grounding and URL context
Section titled “Grounding and URL context”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.
Code execution
Section titled “Code execution”{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.
Context caching
Section titled “Context caching”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.
Generated media
Section titled “Generated media”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.
Embedding models
Section titled “Embedding models”Embedders resolve on demand exactly as models do. These are the IDs the plugin curates:
| Embedder ID | Dimensions | Input |
|---|---|---|
gemini-embedding-2 | 3072 | text, image, video |
gemini-embedding-001 | 3072 | text |
text-embedding-005 | 768 | text |
text-embedding-004 | 768 | text |
text-multilingual-embedding-002 | 768 | text |
multimodalembedding | 768 | text, 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.
Describing a model or embedder
Section titled “Describing a model or embedder”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.
Deprecated helpers
Section titled “Deprecated helpers”| Deprecated | Use instead |
|---|---|
(*GoogleAI).DefineModel | the Models map |
(*GoogleAI).DefineEmbedder | the Embedders map |
(*GoogleAI).IsDefinedEmbedder | drop the call |
googlegenai.GoogleAIModel | genkit.LookupModel |
googlegenai.GoogleAIEmbedder | genkit.LookupEmbedder |
googlegenai.GoogleAIModelRef | googlegenai.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.
Blocked responses
Section titled “Blocked responses”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.
Rate limits
Section titled “Rate limits”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.
The SDK client
Section titled “The SDK client”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.
Next steps
Section titled “Next steps”- Learn about generating content to understand how to use these models effectively
- Explore creating flows to build structured AI workflows
- Read the plugin reference for the full surface
- To use the Gemini API at enterprise scale see the Vertex AI plugin