Releases: genkit-ai/genkit
Release list
Genkit Python SDK v0.11.0
Genkit Python SDK v0.11.0 Release Notes
Genkit Python SDK v0.11.0 is here. This release is about generate() keeping the reply when something goes wrong after the model has already answered, Veo as a background job you poll, and provider failures arriving as one GenkitError you can actually branch on.
uv add genkit genkit-google-genaiWhat's New
generate() returns the leftover instead of throwing (#6100, #6271)
generate() used to throw when the model refused, hit max tool turns, or wrote something that was not the schema you asked for. That discarded the reply. After this, you get the ModelResponse back. This is extremely helpful when you want to retry from the tool rounds that already succeeded.
from pydantic import BaseModel, Field
from genkit import Genkit
from genkit_google_genai import GoogleAI
ai = Genkit(
plugins=[GoogleAI()],
model=GoogleAI.gemini_model('gemini-flash-latest'),
)
class Recipe(BaseModel):
title: str
minutes: int = Field(description='time to cook')
response = await ai.generate(prompt='give me a recipe', output_schema=Recipe)
if response.output is not None:
print(response.output.title)
else:
# Still have the conversation. Send it back and try again.
print(response.finish_reason, response.text)
retry = await ai.generate(
messages=response.messages,
prompt='that was not a recipe. try again as JSON.',
output_schema=Recipe,
)
print(retry.output)Matching JSON is still stop, and .output is the Recipe. A leftover that is not a Recipe comes back as finish_reason=failed; the leftover stays on .text. A safety refusal stays blocked. Hitting max tool turns is aborted. A tool name that is not registered is failed.
.messages only keeps completed rounds (model message plus every tool response). An unanswered tool request is dropped, including the model message that opened it, because no provider will accept that history. response.message is None in those cases.
generate() still throws when there was no turn: empty messages, an unknown output format or model, or an output schema that is not JSON Schema. A tool that raises still raises.
Veo and background models (#6129, #6130, #6131, #6237)
Video is a job, not a round-trip. generate_operation starts it and hands back a ticket; check_operation is how you find out when the video is ready.
operation = await ai.generate_operation(
model=GoogleAI.veo_model('veo-3.1-fast-generate-preview'),
prompt='A paper airplane gliding through a bright classroom',
)
while not operation.done:
operation = await ai.check_operation(operation)
print(operation.output.media[0].url)generate() on a Veo model returns a ModelResponse whose .operation is the ticket and whose .message is None. It does not poll.
GoogleAI.veo_model / VertexAI.veo_model stamp the plugin namespace. On Vertex, a finished operation unpacks uri / GCS / inline bytes into a playable media.url. If safety filters drop every sample, that comes back as an error instead of an empty success.
check_operation and cancel_operation take a live Operation. Reload a persist dump with Operation.model_validate(dumped) first.
One GenkitError from every provider (#6098)
A Gemini 400, an OpenAI 429, an Ollama mid-stream failure, and an Anthropic 529 used to leak as whatever that SDK raises. Retry then retried a bad request, and your except had to know three exception types.
After this, plugins wrap provider HTTP errors as GenkitError. You write one branch. Retry already skips INVALID_ARGUMENT and waits out UNAVAILABLE / RESOURCE_EXHAUSTED, so a lot of that code goes away.
from genkit import Genkit, GenkitError
from genkit_google_genai import GoogleAI
from genkit_middleware import Retry
ai = Genkit(plugins=[GoogleAI()])
try:
response = await ai.generate(
model=GoogleAI.gemini_model('gemini-flash-latest'),
prompt='Draft a weekend in Paris.',
use=[Retry()],
)
print(response.text)
except GenkitError as err:
# Same type whether this was Gemini, Claude, or GPT.
print(err.status, err.original_message)
if err.status == 'RESOURCE_EXHAUSTED' and err.response_metadata:
print('retry after', err.response_metadata.get('retry_after_ms'), 'ms')Status names, not raw HTTP codes:
- 400 →
INVALID_ARGUMENT(retry skips) - 429 +
Retry-After: 60→RESOURCE_EXHAUSTED(wait 60s, then retry) - 503 →
UNAVAILABLE(retry) - Anthropic 529 →
UNAVAILABLE(overloaded) - Anthropic 404 →
NOT_FOUND(Fallbackcan try the next model)
The reflection wire shows the provider text (original_message), not the SDK repr. Veo start() and check() polling are wrapped the same way.
Typed model refs (#6104, #6105, #6074, #6138)
model= now takes a name or a ModelRef. The constructor is what picks the config type, so a Gemini temperature and a Claude max_output_tokens cannot silently ride onto the wrong model. Same slot on Genkit(), generate, generate_stream, generate_operation, define_prompt, and agents.
from genkit import Genkit
from genkit_anthropic import Anthropic, AnthropicConfig
from genkit_google_genai import GeminiConfigSchema, GoogleAI, VertexAI
from genkit_openai import OpenAI, OpenAIConfig
flash = GoogleAI.gemini_model(
'gemini-flash-latest',
config=GeminiConfigSchema(temperature=0.2),
)
vertex_flash = VertexAI.gemini_model('gemini-flash-latest')
veo = GoogleAI.veo_model('veo-3.1-fast-generate-preview')
sonnet = Anthropic.claude_model(
'claude-sonnet-4-5',
config=AnthropicConfig(max_output_tokens=1024),
)
gpt = OpenAI.gpt_model('gpt-4o', config=OpenAIConfig(temperature=0.2))
ai = Genkit(plugins=[GoogleAI(), Anthropic(), OpenAI()], model=flash)
print((await ai.generate(model=flash, prompt='Say hi in one word.')).text)
print((await ai.generate(model=sonnet, prompt='Say hi in one word.')).text)
print((await ai.generate(model=gpt, prompt='Say hi in one word.')).text)A pasted vertexai/… or models/… prefix is stripped and this plugin's namespace is stamped. Family constructors refuse ids they do not own (GoogleAI.gemini_model('veo-3.1-generate-001') tells you to use veo_model). Bedrock and Ollama still take a string name; they do not have a ref constructor yet.
A typed config object has to belong to the model this call is about to hit. A dict, None, or omitting config is left alone.
await ai.generate(model=flash, prompt='hi', config=OpenAIConfig())
# GenkitError: googleai/gemini-flash-latest: config must be
# genkit_google_genai.GeminiConfigSchema or a mapping, got genkit_openai.OpenAIConfigVeneer config= is ModelConfigDict, so autocomplete works at generate() (#5989).
Debug logs in the Dev UI (#6099)
Under genkit start, generate() leaves named breadcrumbs on the span in the trace viewer's Logs panel: generate request resolved, calling model, executing tool requests, model responded. The terminal still honours GENKIT_LOG, so you can keep the shared TTY quiet and still see the trail next to the span.
Fixes & Polish
- Telemetry export no longer stalls
generate()or dumpshttpxtracebacks onto the sharedgenkit startTTY (#5978). - The Python reflection server no longer sends a wildcard
Access-Control-Allow-Origin(#6198). - OpenAI
gpt-image-1no longer sendsresponse_format(the endpoint rejects it). DALL-E still defaults tob64_json(#6167). - OpenAI streaming now requests and populates token usage (
include_usage) (#6229). - Responses-API-only ids (
gpt-5.1-codex,o3-pro, …) are gone from the OpenAI chat catalog. This plugin speaks Chat Completions (#6223). - Whisper honours
config={'translate': True}and routes to the translations API.gpt-4o-transcriberejectstranslateasINVALID_ARGUMENT(#6168).
Existing API Changes
generate() no longer throws after the model has already replied. Code that caught a schema-mismatch, blocked, max-turns, or unknown-tool exception will not see it. Branch on the response:
# OLD (v0.10.0): leftover / blocked / max turns raised
try:
response = await ai.generate(prompt='give me a recipe', output_schema=Recipe)
recipe = response.output
except GenkitError:
...
# NEW (v0.11.0): the leftover is on the response
response = await ai.generate(prompt='give me a recipe', output_schema=Recipe)
if response.output is not None:
recipe = response.output
else:
print(response.finish_reason, response.text)
print(response.messages)Provider HTTP failures are GenkitError, not the raw SDK exception. Code that caught APIError / BadRequestError / APIStatusError from a plugin call will not see those types. Branch on err.status:
# OLD (v0.10.0): caught provider-specific HTTP exceptions
from google.genai.errors import ClientError
from openai import BadRequestError
try:
response = await ai.generate(prompt='Draft a weekend in Paris.')
except (ClientError, BadRequestError) as err:
...
# NEW (v0.11.0): catch one GenkitError and branch on status
from genkit import GenkitError
try:
response = await ai.generate(prompt='Draft a weekend in Paris.')
except GenkitError as err:
if err.status == 'INVALID_ARGUMENT':
...generate() on a Veo (or any background) model returns a ticket. .message is None. Use generate_operation / check_operation to poll:
# OLD (v0.10.0): background video models were unsupported
# NEW (v0.11.0): generate_operation returns a ticket; poll until done
operation = await ai.generate_operation(
model=GoogleAI.veo_model('veo-3.1-fast-generate-preview'),
prompt='A paper airplane gliding through a bright classroom',
)
while not operation.done:
operation = await ai.check_operation(operation)
print(operation.output.media[0].url)**`check_operation...
Genkit Go v1.13.1
v1.13.0 is retracted. It ships the A2UI preview at github.com/firebase/genkit/go/plugins/a2ui, while its release notes describe github.com/firebase/genkit/go/plugins/a2ui/exp. This release moves the package to the documented path and records the retraction in go.mod, so go get github.com/firebase/genkit/go@latest resolves here and version listings hide v1.13.0. Everything else in the v1.13.0 notes applies unchanged.
import a2uix "github.com/firebase/genkit/go/plugins/a2ui/exp"What's Changed
- refactor(go/plugins/a2ui): move the plugin to
a2ui/expwhile it is in preview by @apascal07 in #6275 - chore(go): retract v1.13.0 by @apascal07 in #6276
Full Changelog: go/v1.13.0...go/v1.13.1
Genkit Go v1.13.0
Warning
Retracted. This version ships the A2UI preview at github.com/firebase/genkit/go/plugins/a2ui, not at the plugins/a2ui/exp path the notes below describe. Use v1.13.1: it moves the package to the documented path and retracts this version in go.mod. Everything else below applies to v1.13.1 unchanged.
Progress survives failure. A generate call that fails or is stopped returns the conversation up to its last completed tool round, beside the classified error. An agent commits that conversation as a failed or aborted snapshot, and both resume. Sub-agents run in the background, get waited on or aborted, and pick up where they left off from any process holding the task ID. Beyond that, the experimental A2UI plugin lets an agent stream interactive UI to a browser.
go get github.com/firebase/genkit/go@v1.13.0Generate returns what it finished, even on failure
Once the request has resolved, Generate returns the partial response beside its error:
resp, err := genkit.Generate(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("Plan the trip."),
ai.WithTools(searchFlights, bookHotel),
ai.WithMaxTurns(3),
)
if err != nil && resp != nil {
resp.History() // The completed rounds. Send them back to retry the failed step.
resp.FinishReason // Failed if something broke, Aborted if the caller stopped it.
resp.FinishMessage // The cause, e.g. "exceeded maximum tool call iterations (3)".
resp.Error // The same cause, classified: Status, Message, Details.
}ai.FinishReasonFailed means something broke: a model call or a tool. ai.FinishReasonAborted means the caller stopped it: a cancelled context, an expired deadline, or a limit such as WithMaxTurns. resp.Error is the classified form of FinishMessage, so a response read back from a trace or a persisted turn still says why it stopped.
History() ends at a turn seam: the completed rounds of model message plus every tool response, and nothing from the turn that failed. No provider accepts a conversation ending in an unanswered tool request, so a failed tool drops its whole round, including the model message that opened it. Send the history back to retry the failed step without repeating the tool calls that succeeded. Text streamed before the failure already reached your callback.
GenerateStream and GenerateDataStream yield the same partial beside their error, Done and carrying Response.
Values survive the action boundary
Action.Run zeroed its output on any error, the JSON surface marshaled nothing, and the trace recorded output only on success. All three now carry whatever the function returned: a flow that returns a value beside an error hands it to its caller, an output that failed schema validation comes back with its error, and a failed generate's conversation shows up in the Dev UI trace.
A blocked response is an error, not a schema mismatch
GenerateData, GenerateDataStream, DataPrompt.Execute, and DataPrompt.ExecuteStream parsed a safety-blocked response and reported Expected: object, given: null. They now return ai.ErrGenerationBlocked, a FAILED_PRECONDITION subtype, with the response alongside:
out, resp, err := genkit.GenerateData[Itinerary](ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("Plan a week in Kyoto."),
)
if errors.Is(err, ai.ErrGenerationBlocked) {
log.Printf("refused: %s", resp.FinishMessage)
}Interrupts, tool requests, and empty responses keep a nil output and a nil error. Streamed chunks parse before any finish reason exists, so the terminal value settles the call. Generate still hands a blocked response back as a value.
Resume any turn, whether it succeeded, failed, or was aborted
Agents in ai/exp build on the partial. A failed turn commits the tool rounds it completed as a failed snapshot carrying the error, and resume accepts it:
out, _ := agent.RunText(ctx, "Book the full itinerary.")
if out.FinishReason == aix.AgentFinishReasonFailed {
snap, _ := agent.GetSnapshot(ctx, out.SnapshotID)
snap.Status // aix.SnapshotStatusFailed, no longer a dead end
snap.Error // the classified failure, same as out.Error
snap.State.Messages // the tool rounds the turn completed
}Re-attempt with an input that has no payload. The turn runs again on the committed messages, so the tool calls that succeeded are not repeated:
retried, err := agent.Run(ctx, &aix.AgentInput{}, aix.WithSessionID[any](out.SessionID))A new message works on that snapshot too, and rewinding past the failure is a resume from the previous snapshot ID. Whether to retry is your call: the runtime records the status and never judges it.
Every failure past the model call commits. A turn rejected before it reached the model rolls back, and the resume point stays the turn before. AgentOutput.SnapshotID names the latest resumable state either way. Custom agents opt in by returning a TurnResult beside the error; a bare error still discards the turn. Without a store, the failed output's State carries the same resume point inline.
The turn-end snapshot now writes on a context that outlives the turn's own, so a turn cancelled from outside still lands its snapshot.
aborted means the caller stopped it
aborted now covers every way a caller ends a run, and failed every way one breaks. A cancelled context, a closed transport, an expired deadline, a limit such as ai.WithMaxTurns, or Abort on a detached run: each lands an aborted snapshot holding the turns that finished, and each resumes like a failed one. Run returns that snapshot's output beside the error instead of nil:
ctx, cancel := context.WithCancel(ctx)
out, err := chatAgent.Run(ctx, &aix.AgentInput{Message: msg}) // cancel() elsewhere
// err is what stopped the run; out names where it stopped.
if out.FinishReason == aix.AgentFinishReasonAborted {
resumed, _ := chatAgent.Run(context.Background(), &aix.AgentInput{},
aix.WithSnapshotID[any](out.SnapshotID))
}The turn in flight is discarded whole. A tool that ran inside it runs again on resume.
Wind-down has its own status: aborting
A detached run reaches aborted in two writes: the flip that stops the work, and the finalize that stamps the state on. The row between them was aborted with no state, shaped as pending. It is now aborting, a shared wire status. The worker keeps heartbeating through its wind-down for up to five minutes, so a wedged drain reads as expired instead of hanging forever. WaitForSnapshot waits through the window, and the abort companion answers aborting where it answered aborted.
The basic-agents CLI shows all of it: a broken or stopped turn is offered like any other, and an empty line re-runs the turn it left unanswered.
Sub-agents run in the background and pick up where they left off
Reach any agent by name with AgentHandle
AgentHandle is the caller-side view of an agent for code that knows it only by name (orchestrators, middleware, tools), with custom state as json.RawMessage. One lookup replaces the action lookup, the BidiAction assertion, and the JSON marshaling:
h := genkitx.LookupAgent(g, "researcher") // nil on a miss; or agent.Handle()
out, err := h.RunText(ctx, task,
aix.WithState(&aix.SessionState[json.RawMessage]{Messages: history}))RunDetached is the one-shot counterpart of AgentConnection.Detach. A DetachedTask is a snapshot ID plus the agent that minted it, so any process can rehydrate it:
task, err := agent.RunDetached(ctx, &aix.AgentInput{Message: msg})
id := task.SnapshotID() // record it
task = agent.Task(id) // any process, any time later
snap, err := task.Poll(ctx) // one read
snap, err = task.Wait(ctx) // blocks until it settles
status, err := task.Abort(ctx)POST /agents/{name}/waitForSnapshot is the blocking counterpart of getSnapshot: one request follows a detached run to completion, and a trace carries one span per wait instead of one per tick. Handle calls are shaped like a remote client's: the state transform applies, a stale pending row reads as expired, and errors match by status name, so an HTTP-backed handle is a second implementation rather than a second surface.
GetSnapshot, GetLatestSnapshot, and Poll take aix.WithMetadataOnly(), which returns status, finish reason, parent, and timestamps without the conversation. Stores that implement the optional SnapshotMetadataReader (the bundled local stores and Firestore) skip loading the history; Firestore answers with one document read. Other stores keep compiling and are read in full.
Delegate without waiting
With Async set on the Agents middleware, every delegation tool takes a background flag that returns a task ID at once, and three shared tools control what was launched:
researcher := genkitx.DefineAgent(g, "researcher",
aix.InlinePrompt{
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithSystem("You are a thorough research assistant."),
},
aix.WithDescription[any]("Researches a topic and summarizes well-sourced findings."),
// A background delegation is tracked by a snapshot, so the sub-agent needs a store.
aix.WithSessionStore(localstore.NewInMemorySessionStore[any]()),
)
ai.WithUse(&middlewarex.Agents{
Agents: []aix.AgentRef{researcher.Ref()},
Async: true,
})delegate_to_researcher {task, name?, background: true}
-> {response, taskId: "researcher:<snapshotId>", status: "pending"}
check_background_tasks {taskIds}
-> {tasks: [{taskId, agent, status, response?, artifacts?, error?}]}
wait_for_background...
Genkit JS 1.42.0
New: A2UI plugin (@genkit-ai/a2ui)
A new plugin that lets an agent stream A2UI ("Agent to UI") surfaces, rich interactive UI rendered incrementally by the client, instead of just prose. Server integration is a single model middleware you drop into an agent's use array. It injects the catalog's capabilities into the system prompt, then intercepts streamed and final model output, extracts a2ui blocks, validates them against the catalog, and rewrites them into A2UI data parts. A browser-safe /client entrypoint (a2uiEnvelopes, actionToMessage) feeds envelopes to a renderer and sends user actions back to the agent. Ships with a runnable js/testapps/a2ui sample (Express backend + Vite/Lit frontend). Note: this is an early proof-of-concept. (#5795, #6209, #6222)
import { genkit } from 'genkit/beta';
import { googleAI } from '@genkit-ai/google-genai';
import { a2ui } from '@genkit-ai/a2ui';
const ai = genkit({ plugins: [googleAI()] });
export const uiAgent = ai.defineAgent({
name: 'uiAgent',
model: 'googleai/gemini-flash-latest',
system: 'You help users. Render UI when it is clearer than prose.',
use: [a2ui()], // the only A2UI-specific line
});Features
- OpenTelemetry logs are now enabled by default; the
GENKIT_OTEL_ENABLE_LOGSflag has been removed, and custom log record processors set intelemetryConfigare no longer overwritten. (#6069)
Bug Fixes
- plugins/google-genai: Surface API errors from streaming responses by mapping HTTP status codes to Genkit statuses and parsing JSON error bodies returned with HTTP 200 (e.g. overloaded models), so middleware like retry can react appropriately. (#6170)
- plugins/compat-oai: Fix inverted 401/403 status mapping so a bad or missing API key now surfaces as
UNAUTHENTICATEDinstead ofPERMISSION_DENIED; affects all compat-oai based providers (openai, xai, deepseek). (#6151) - Add prompt name to
renderspan metadata for better Dev UI usability. (#6025)
Full Changelog: https://github.com/genkit-ai/genkit/compare/genkit@1.41.0...genkit@1.42.0
Genkit CLI and Dev UI 1.42.0
CLI
no changes
Developer UI
New features
- OpenTelemetry log messages are now visible in the Dev UI. Structured log records emitted by your app show up in the Logs panel, with each record's attributes previewed inline. Stay tunes for next JS/Go releases to start seeing actual logs.
- Errors are labeled with their reported status. The error callout now prefixes a failed action with the canonical status the runtime returned, so a rejected request reads differently from a backend outage (for example
Invalid Argument: at least one message is required in generate request). - Agent runner now supports a session ID in AgentInit.
- Agent runner shows a custom patch error indicator. When a
customPatchfails to apply, the runner surfaces an icon and tooltip, and resets state for the next turn.
Fixes
- Prose renders alongside data parts. A message that mixes text and data parts now renders both in order, instead of dropping the surrounding text.
- Numeric model config values are sent as numbers. Numeric options such as temperature and topK now reach the runtime as numbers rather than strings, so strongly typed runtimes no longer reject them; stepping on float fields is fixed too.
- "Open in prompt runner" works for render spans. It now resolves rendered prompts in agent traces, and the button is hidden when no target action can be found.
- Tool responses show in the prompt runner. Tool responses now render as tool action cards for both pending and resumed actions.
Genkit Python SDK v0.10.0
Release Highlights: Genkit Python SDK v0.10.0
We are excited to announce the v0.10.0 release of the Genkit Python SDK!
This release expands Genkit’s multi-cloud footprint with the official launch of the Amazon Bedrock Plugin.
This release also brings enterprise-grade Firestore agent persistence to Python with the Firestore Session Store, achieves full cross-SDK Agent Conformance parity, and improves the local developer experience by reducing noisy logging in Terminal.
Major Highlights
1. Official Amazon Bedrock Plugin (genkit-amazon-bedrock)
Developers building on AWS can now leverage Genkit's unified abstractions across Bedrock-hosted foundation models:
- Converse & ConverseStream APIs: Unified text generation, streaming, tool-calling, and multimodal input across Anthropic Claude on Bedrock, Amazon Nova, Amazon Titan, Mistral, and Cohere.
- Embeddings & Reranking: Direct integration with Amazon Titan Embeddings (v1/v2) and Cohere Rerank for high-precision RAG pipelines.
- Image Generation: First-class support for Amazon Titan Image Generator and Stability AI (SDXL).
- Async Boto3 Engine: High-throughput async client dispatch with automatic AWS credential resolution (IAM roles, SSO, environment variables) and exponential backoff retry.
from genkit import Genkit
from genkit_amazon_bedrock import Bedrock
# 1. Initialize Bedrock (uses ambient AWS credentials and region)
ai = Genkit(plugins=[Bedrock()])
# 2. Generate with flagship Bedrock models
res = await ai.generate(
model='bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0',
prompt='Summarize the benefits of declarative AI orchestration in three bullets.',
)
# 3. Inspect the output
print(res.text)
# => • Type-safe tool definitions and schema validation
# • Cloud-agnostic provider switching without code changes
# • Integrated observability and multi-turn state management2. Enterprise Agent Persistence: FirestoreSessionStore
Building production-grade conversational agents requires durable state management that survives container restarts, scales horizontally across serverless workers (Cloud Run, Cloud Functions), and doesn't degrade on long conversations.
Unlike naive single-document session stores that quickly hit Firestore's 1 MiB document limit and incur heavy read/write costs, the new FirestoreSessionStore in genkit-google-cloud is engineered for long-lived sessions:
- Incremental Diffs & Sharded Checkpoints: Persists each turn as an incremental RFC 6902 JSON Patch diff anchored to periodic full-state checkpoints, so state is sharded and sessions scale indefinitely without hitting document size limits.
- Bounded Document I/O: The number of documents read or written per turn is bounded by
checkpoint_interval(default 25) rather than total conversation length, keeping latency and Firestore costs predictable. - Zero Secondary Indexes (Strong Consistency): State reconstruction and turn lookups use only direct document-ID fetches inside read transactions, requiring no composite indexes.
- Atomic Transactions: Snapshot writes and session pointer updates commit together in atomic Firestore transactions with automatic retry handling on concurrent writes.
- Realtime Status Streaming: Uses native Firestore listeners (
on_snapshot_status_change) to stream live turn statuses across processes and support distributed aborts.
from genkit import Genkit
from genkit_google_cloud import FirestoreSessionStore
from genkit_google_genai import GoogleAI
ai = Genkit(plugins=[GoogleAI()])
# 1. Instantiate persistent Firestore session store
store = FirestoreSessionStore(collection='agent_sessions')
# 2. Define an agent with Firestore persistence
agent = ai.define_agent(
name='customer_assistant',
model='googleai/gemini-pro-latest',
system='You are a helpful customer support assistant.',
store=store,
)
# 3. Resume multi-turn conversations seamlessly across distributed instances
chat = agent.chat(session_id='session_user_12345')
response = await chat.send('Where did we leave off on my order?')3. Pinned Catalog Entries: Gemini 3.6 & 3.7 Flash
While dynamic aliases like googleai/gemini-flash-latest automatically point to the current Gemini Flash release, we have officially added googleai/gemini-3.6-flash and googleai/gemini-3.7-flash to the known models catalog in genkit-google-genai. This allows production applications to explicitly pin their model versions for reproducible inference and regression testing.
4. Cross-SDK Agent Conformance & Protocol Hardening
- Shared Conformance Harness: Added an automated cross-SDK test suite (
agent_conformance_test.py) validating the Python SDK against the shared Genkit agent specification (agents.yaml). - Turn Lifecycle & Aborts: Hardened client-side and server-side abort signal handling, turn snapshot serialization, and session branch resumes.
- Actionable Tool Restart Errors: When resuming interrupted turns, underlying reasons (such as missing
toolApprovedmetadata) are now surfaced clearly inGenkitErrorrather than as generic runtime failures.
🔧 Developer Experience & Terminal Logging
- Refined Local Terminal Logging: Cleaned up the local development logging experience by demoting verbose framework discovery and health-check chatter on the shared terminal console (#5979).
- Sanitized Model Payloads: Debug logs now suppress dumping full prompt strings and raw model response bodies by default, protecting sensitive developer data and keeping terminal streams readable (#5968).
- Standardized Error Taxonomy: Model resolution failures now cleanly raise
GenkitError(status='NOT_FOUND')instead of uncaught key errors (#5982). - Dev UI Model Capabilities: Serialized
ModelInfowith camelCase aliases, ensuring model features (tool calling, streaming, multimodality) render correctly in the Developer UI (#5964).
Breaking Changes
Custom SessionStore Protocol Contract Update
For developers implementing custom session persistence backends (e.g. for Redis, Postgres, or DynamoDB), the SessionStore protocol has been updated to support cross-process snapshot status subscriptions (SnapshotSubscriber) and strict state transition invariants (#6028). Custom store implementations will need to update their class signatures to implement these lifecycle methods.
Package Releases
| Package | Version |
|---|---|
genkit (Core SDK) |
0.10.0 |
genkit-amazon-bedrock (New) |
0.10.0 |
genkit-anthropic |
0.10.0 |
genkit-django |
0.10.0 |
genkit-evaluators |
0.10.0 |
genkit-fastapi |
0.10.0 |
genkit-flask |
0.10.0 |
genkit-google-cloud |
0.10.0 |
genkit-google-genai |
0.10.0 |
genkit-middleware |
0.10.0 |
genkit-ollama |
0.10.0 |
genkit-openai |
0.10.0 |
genkit-vertexai |
0.10.0 |
Genkit Go v1.12.0
xAI, DeepSeek, DashScope, Kimi, Z.ai, and OpenRouter join the OpenAI-compatible family, all of it rebuilt on typed per-provider configs that the framework validates before a request is billed. Failures now carry a status from the line that raised them through the retry middleware to the HTTP response, and provider SDK errors arrive already classified. Logs attach to the span that produced them. Prompt content functions are typed against the prompt's own input. Options that used to reject a repeat now merge.
go get github.com/firebase/genkit/go@v1.12.0Six providers join the OpenAI-compatible core
Genkit Go ships plugins for xAI, DeepSeek, DashScope (Qwen), Kimi, Z.ai (GLM), and OpenRouter. They sit beside openai and the OpenAI-compatible anthropic plugin on a rebuilt compat_oai core.
Each of the six declares a ChatConfig covering exactly the fields its provider documents: Kimi's K-series takes no temperature, Z.ai caps it at 1, DeepSeek carries a user_id that partitions its context cache. The plugin advertises the JSON schema inferred from that struct and the framework enforces it at the action boundary, so an out-of-range value fails before the request is billed, and the Dev UI renders the same schema as a form.
g := genkit.Init(ctx, genkit.WithPlugins(&deepseek.DeepSeek{})) // DEEPSEEK_API_KEY
model := deepseek.ModelRef("deepseek-v4-pro", &deepseek.ChatConfig{
ReasoningEffort: deepseek.ReasoningEffortMax,
})g := genkit.Init(ctx, genkit.WithPlugins(&kimi.Kimi{})) // KIMI_API_KEY
model := kimi.ModelRef("kimi-k3", &kimi.ChatConfig{
Thinking: &kimi.ThinkingConfig{Type: kimi.ThinkingTypeEnabled},
})g := genkit.Init(ctx, genkit.WithPlugins(&xai.XAI{})) // XAI_API_KEY
model := xai.ModelRef("grok-4.6", &xai.ChatConfig{
ReasoningEffort: xai.ReasoningEffortXHigh,
})g := genkit.Init(ctx, genkit.WithPlugins(&zai.ZAI{})) // ZAI_API_KEY
model := zai.ModelRef("glm-5.1", &zai.ChatConfig{
Thinking: &zai.ThinkingConfig{Type: zai.ThinkingTypeEnabled},
})g := genkit.Init(ctx, genkit.WithPlugins(&dashscope.DashScope{})) // DASHSCOPE_API_KEY
// openai.Ptr is the openai-go SDK helper for optional fields.
model := dashscope.ModelRef("qwen-plus", &dashscope.ChatConfig{
EnableThinking: openai.Ptr(true),
ThinkingBudget: openai.Ptr(2048),
})OpenRouter reaches the rest
OpenRouter fronts hundreds of models from dozens of vendors. The plugin curates nothing: you name a model, and the ID keeps its upstream vendor prefix, which puts two slashes in an action name such as openrouter/openai/gpt-5. The gateway controls are typed at the call site. Choose which providers may serve the request, chain fallback models, set reasoning effort.
g := genkit.Init(ctx, genkit.WithPlugins(&openrouter.OpenRouter{})) // OPENROUTER_API_KEY
resp, err := genkit.Generate(ctx, g,
ai.WithModel(openrouter.ModelRef("openai/gpt-5", &openrouter.ChatConfig{
// Try these in order if gpt-5 is unavailable.
Models: []string{"anthropic/claude-sonnet-4.5", "deepseek/deepseek-v4-pro"},
Provider: &openrouter.ProviderRouting{
Sort: openrouter.ProviderSortThroughput,
DataCollection: openrouter.DataCollectionDeny,
},
Reasoning: &openrouter.ReasoningConfig{Effort: openrouter.ReasoningEffortHigh},
})),
ai.WithPrompt("Work through this step by step."),
)The family reads a provider's non-standard reasoning field, reasoning_content first and reasoning second, back as a Genkit reasoning part, so resp.Reasoning() covers DeepSeek, Kimi, and OpenRouter alike. A gateway that reports what it charged puts the figure under the cost key of Usage.Custom. Test for the key rather than a nonzero value: a free-tier request is priced at an explicit zero.
fmt.Println(resp.Reasoning())
if cost, ok := resp.Usage.Custom["cost"]; ok {
fmt.Printf("this answer cost %.5f\n", cost)
}Google and Claude, closer to the metal
Vertex AI Express Mode authenticates with an API key alone: no project, no location, no ADC. Both Google backends take BaseURL, Headers, and HTTPClient, so a proxy or a custom transport is a struct field, and GoogleAI takes APIVersion as well. Client() hands back the genai SDK client for Files, Caches, Batches, and Tunings. When a 429 names its own backoff, RetryDelay reads it.
gemini := &googlegenai.GoogleAI{
APIVersion: "v1alpha",
Headers: http.Header{"X-Team": {"platform"}},
}
// Express Mode: a Vertex AI key, no project, no location, no ADC.
vertex := &googlegenai.VertexAI{APIKey: "YOUR_VERTEX_API_KEY"}
g := genkit.Init(ctx, genkit.WithPlugins(gemini, vertex))
client, err := gemini.Client()
if err != nil {
return err
}
file, err := client.Files.UploadFromPath(ctx, "photo.jpg", &genai.UploadFileConfig{
MIMEType: "image/jpeg",
})
_, err = genkit.Generate(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("Explain how neural networks learn."),
)
if err != nil {
if delay, ok := googlegenai.RetryDelay(err); ok {
time.Sleep(delay) // the service named its own backoff
}
return err
}On the native Claude plugin, Opts carries anthropic-sdk-go request options into the client: retries, timeouts, middleware, and the SDK's Bedrock and Vertex routing helpers. ModelRef binds a *anthropic.MessageNewParams to a model ID, so thinking, effort, and server-side tools are typed at the call site.
g := genkit.Init(ctx, genkit.WithPlugins(&anthropic.Anthropic{
Opts: []option.RequestOption{option.WithMaxRetries(5)},
}))
resp, err := genkit.Generate(ctx, g,
ai.WithModel(anthropic.ModelRef("claude-sonnet-5", &sdk.MessageNewParams{
MaxTokens: 4000,
Thinking: sdk.ThinkingConfigParamUnion{
OfAdaptive: &sdk.ThinkingConfigAdaptiveParam{},
},
OutputConfig: sdk.OutputConfigParam{Effort: sdk.OutputConfigEffortHigh},
})),
ai.WithPrompt("Plan the migration."),
)Every model plugin, Google and Claude included, takes a Models map keyed by model ID that overrides the capabilities the plugin resolves. Fields left at zero keep what the plugin already knows, so one entry describes a model released after the plugin without forking it.
Classify a failure once, and it stays classified
Genkit has one error type and one status vocabulary: InvalidArgument, NotFound, PermissionDenied, Unauthenticated, ResourceExhausted, Unavailable, DeadlineExceeded, Internal, and nine more. The names follow the Google API error model and mean the same thing in the JS and Python runtimes. Classify a failure at the point where you know what it is.
// Keeps NOT_FOUND from its parent, and matches both itself and status.ErrNotFound.
var ErrRecipeNotFound = status.ErrNotFound.Subtype("recipe not found")
func lookupRecipe(dish string) (string, error) {
recipe, ok := cookbook[dish]
if !ok {
return "", status.PublicErrorf(ErrRecipeNotFound, "no recipe for %q", dish)
}
return recipe, nil
}
func plateUp(dish string) (string, error) {
recipe, err := lookupRecipe(dish)
if err != nil {
return "", fmt.Errorf("plating %q: %w", dish, err)
}
return recipe, nil
}Subtype derives a sentinel that keeps its parent status. Errorf builds a message for your logs, PublicErrorf marks one safe to hand a caller. Adding context with %w changes nothing else: the sentinel, the status, and the public message all survive the trip up the stack. No re-wrapping, no matching on message text.
switch {
case errors.Is(err, ErrRecipeNotFound): // this exact failure
case errors.Is(err, status.ErrNotFound): // anything missing
case errors.Is(err, status.ErrResourceExhausted): // rate limited or out of quota
}
status.Of(err) // status.NotFound
status.Of(err).HTTPCode() // 404
msg, public := status.PublicMessage(err) // `no recipe for "lasagna"`, trueCode you did not write reads the classification
Serve the flow with genkit.Handler and the response code comes from the classification. The message only leaves the process when you built it with PublicErrorf.
genkit.DefineFlow(g, "recipe", func(ctx context.Context, dish string) (string, error) {
if dish == "" {
// 400, body: dish must not be empty
return "", status.PublicErrorf(status.ErrInvalidArgument, "dish must not be empty")
}
if dish == "escargot" {
// 400, body: invalid argument
return "", status.Errorf(status.ErrInvalidArgument, "supplier for %q is offline", dish)
}
// 500, body: internal
return "", errors.New("db at 10.0.0.3: password rejected")
})Everything else is redacted to the generic label for its status, and the full text goes to the server log. Set GENKIT_ENV=dev and the real message comes back instead; the code is the same either way.
The reach of that classification is what changed. Every plugin now classifies what its provider SDK returns, so a 401 from Anthropic or a 429 from Gemini arrives already carrying Unauthenticated or ResourceExhausted. Retry and Fallback have always read a classification and disagreed on purpose: Retry reissues a ResourceExhausted or Unavailable call, leaves an InvalidArgument alone, and retries an unclassified error because a dial timeout deserves another attempt, while Fallback propagates an unclassified error rather than spending a second billed model on it. What is different is that provider failures now reach them classified instead of opaque.
resp, err := genkit.Generate(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("Draft the menu."),
ai.WithUse(
&middleware.Retry{MaxRetries: 3},
&middleware.Fallback{Models: []ai.ModelRef{
ai.NewModelRef("googleai/gemini-3.5-flash", nil),
}},
),
)Logs that land on the trace
Ev...
Genkit JS and CLI 1.41.0
JS SDK
-
New GPT-5.x models in the OpenAI-compatible plugin. Added
gpt-5.2,gpt-5.4,gpt-5.4-mini,gpt-5.4-nano, andgpt-5.5tocompat-oai, so you can reach the latest OpenAI models without waiting on a plugin bump. (#5389) -
Per-request Anthropic API keys. The Anthropic plugin now resolves the API key lazily at request time instead of only at init, so you can override the key per call. Handy for multi-tenant setups and key rotation. (#4298)
-
Clearer retry and fallback logs. The
retryandfallbackmodel middleware now emit structured warnings explaining what happened: which attempt, how long the backoff is, why a retry was skipped, and which model it's falling back to.AbortErrorandToolInterruptErrorare now correctly treated as non-retryable. (#5449) -
Fixed audio transcription file extensions.
compat-oaispeech-to-text now derives the right file extension from the content type (.mp3,.wav,.webm, and friends) instead of a generic name, which fixes transcription failures with providers that key off the extension. (#3367) -
More reliable agent resume.
validateResumeAgainstHistorynow searches history newest-first, fixing an edge case when resuming interrupted conversations. (#5832) -
Options for checkOperation and cancelOperation. You can now pass options (including per-request client config) when polling or cancelling long-running operations. Auth and secrets are also now scrubbed from context logging. (#5972)
-
Security hardening for background operations (heads up: minor breaking change). The google-genai plugin no longer persists
clientOptionsin operation metadata, which previously risked leaking API keys if you returned a full operation object to a client. If you use per-request overrides (like a custom API key) with long-running models, you now need to pass those same overrides to eachcheckOperation/cancelOperationcall. (#5992) -
ROUGE evaluator fix on Vertex AI. Corrected the type used by the ROUGE score evaluator and cleaned up duplicated evaluator types. (#3592)
CLI
-
Structured output now survives "Export to .prompt". When you export from the Dev UI model runner, the output schema is included in the front matter (written as Picoschema) instead of being dropped, so exported prompts keep their structured output config. (#5506)
-
Cleaner
trace:getoutput.genkit trace:get <id>gets a readable tree view, and--format jsongives you clean JSON that's easy to pipe into other tools. (#5848) -
More reliable startup in ephemeral runtimes. The CLI now waits for actions to finish registering before proceeding, fixing flaky behavior where actions weren't yet available. (#5949)
Dev UI
This release makes the runners smarter about structured output, sharpens live streaming in the Agent Runner, and clears up a bunch of rough edges in traces, tools, and datasets.
Highlights
- See and edit output config in the Model Runner. The Model Runner now has an editable Output panel. When you load a generation from a trace, you can inspect the inherited format and JSON schema, tweak them, or switch back to plain text for a freeform prompt. The schema shows up as readable JSON Schema instead of a raw Zod blob, and a reset restores the inherited config.
- Structured JSON output, rendered properly. Model Runner output that comes back as JSON is now rendered as clean, formatted JSON instead of a wall of text.
- Output conformance in the Prompt Runner. The Prompt Runner now supports output conformance, so prompts that define a structured schema behave consistently with the rest of the tooling.
- Live streaming patches in the Agent Runner. Agent Runner now supports
customPatchstreaming, so incremental updates from your agent show up in the UI as they arrive.
Improvements & Fixes
- No more garbled streamed text. Fixed a decoding bug where multi-byte characters (CJK, emoji, accented letters) split across network chunks would render as
�. Streamed output now decodes cleanly across chunk boundaries. - Structured output + tools now play nicely together. Schemas are sent under the field the generate action actually reads, so structured output no longer accidentally suppresses tool calls. The tool loop runs and applies the structured result on the final turn.
- Real-time span metadata in traces. In-progress spans now keep their initial metadata (name, path, type) instead of dropping it, and show a clean
--placeholder for duration until the span completes. - Agent Runner session fixes. Switching from multi-turn (bidi) to single-turn mode no longer trips strict backends (e.g. Python/Pydantic) with
extra_forbiddenerrors, session IDs update correctly when a bidi stream ends, and context is preserved when starting a new chat or loading a trace. - Manual tool responses are now sent as proper messages, and tool action cards render correctly when resuming or adding a model message with tool requests.
- Interruptions callout added to the Tools page so it's clearer what's happening when a tool interrupts.
- Dataset sample editor restored to its cleaner previous style, plus tidier JSON editor spacing.
New Contributors
- @MikeRez0 made their first contribution in #5239
- @a2105z made their first contribution in #5891
- @nsrCodes made their first contribution in #3367
Full Changelog: v1.40.1...v1.41.0
Genkit Python SDK v0.9.0
Release Highlights: Genkit Python SDK v0.9.0
We are excited to announce the v0.9.0 release of the Genkit Python SDK! This landmark update introduces the official launch of Agents—bringing first-class, stateful multi-turn AI workflows to Python—alongside a complete plugin package reorganization on PyPI, enhanced observability, and improved provider compatibility.
Major Features
Official Agents Launch
- Stateful Multi-Turn Workflows: Introduces core
AgentandSessionabstractions, providing a stateful, streaming layer built on top ofgenerate. - Pluggable Session Persistence: Support for both server-managed session stores (
InMemorySessionStore,FileSessionStore, etc.) and client-managed state snapshotting. - Human-in-the-Loop Interruption: Built-in support for tool approval and turn interrupts/resumes, allowing human intervention before executing sensitive operations.
- Remote Agent Client & Transports: Seamless communication with local or remote agents over HTTP/WebSocket transports via
remote_agentwith automatic session history management (#5541). - Artifacts & Custom State: Stream, list, and persist session artifacts and custom state updates across multi-turn agent turns.
Plugin Package Reorganization & PyPI Launch
- All plugins are now organized into dedicated, publishable PyPI packages (
genkit-*). - Backward Compatibility: Existing/legacy plugin packages will continue to work seamlessly. When loaded, a friendly warning will prompt developers to update their dependencies to the new package names:
genkit(Core SDK)genkit-anthropic(formerlygenkit-plugin-anthropic)genkit-django(formerlygenkit-plugin-django)genkit-evaluators(formerlygenkit-plugin-evaluators)genkit-fastapi(formerlygenkit-plugin-fastapi)genkit-flask(formerlygenkit-plugin-flask)genkit-google-cloud(formerlygenkit-plugin-google-cloud)genkit-google-genai(formerlygenkit-plugin-google-genai)genkit-middleware(formerlygenkit-plugin-middleware)genkit-ollama(formerlygenkit-plugin-ollama)genkit-openai(formerlygenkit-plugin-openai)genkit-vertexai(formerlygenkit-plugin-vertexai)
Improvements & Bug Fixes
- Session Management & Tracing: Recorded session state on
runTurnspans and added support for client-managedsessionIds(#5871). - Gemini Compatibility:
- Observability: Seeded span attributes at trace start so live traces render immediately in the Developer UI (#5808).
- Runtime & CLI: Muted dev server health poll logs (#5867) and ensured reliable CLI runtime metadata cleanup on SIGINT/SIGTERM (#5773).
- Anthropic Plugin: Added stable and beta API selection (#5752) and extended thinking capabilities.
Genkit Go v1.11.0
What's Changed
- feat(go): implement telemetry label propagation via context by @MichaelDoyle in #5666
- feat(go/plugins/googlegenai): add Vertex AI multi-region and apiVersion support by @adesinah in #5772
- feat(go/plugins/anthropic): register latest Claude models by @adesinah in #5519
- fix(go/ai): allow media URLs in prompts without a content type by @apascal07 in #5793
- fix(go/googlegenai): map tool role to user for Gemini content API by @pavelgj in #5782
- fix(go/plugins/ollama): register embedders by model name, not server address by @IzaakGough in #5648
- fix(agents): reject empty prompt-agent turns by @adesinah in #5744
- docs(go): add experimental tools example and refresh README by @apascal07 in #5640
New Contributors
- @IzaakGough made their first contribution in #5648
Full Changelog: go/v1.10.0...go/v1.11.0
