Skip to content

Add First-Class Gemini SDK Integration to Contrib#1378

Draft
JasonSteving99 wants to merge 5 commits intomainfrom
jason-experiment-gemini-sdk-integration
Draft

Add First-Class Gemini SDK Integration to Contrib#1378
JasonSteving99 wants to merge 5 commits intomainfrom
jason-experiment-gemini-sdk-integration

Conversation

@JasonSteving99
Copy link

Summary

Adds temporalio.contrib.google_gemini_sdk, a drop-in integration that makes the Google Gemini SDK fully durable inside Temporal workflows — every LLM HTTP call and every tool invocation becomes a Temporal activity, with minimal changes to how users write Gemini SDK code.

This is intended to improve on and replace the previously demonstrated approach included in Google's official Gemini docs from our cross-promotion with Google, which required users to manually manage the agentic loop, build a tool registry, and wrap each model call in an activity. The new integration eliminates all that ceremony and allows users to get the full value out of the Gemini SDK while still running on Temporal.

How it works

  • GeminiPlugin — A Temporal Worker plugin that creates and owns the genai.Client, registers the HTTP transport activity, configures the Pydantic data converter, and sets up sandbox passthrough modules. Users pass the same kwargs they'd pass to genai.Client().
  • HTTP-layer interception — A custom httpx.AsyncClient (TemporalHttpxClient) intercepts every HTTP request the Gemini SDK makes and dispatches it as a gemini_api_call activity via workflow.execute_activity. This makes all model calls (including streaming) durable and visible in event history.
  • activity_as_tool() — Converts any @activity.defn function into a Gemini-compatible tool callable. Gemini's automatic function calling (AFC) drives the agentic loop natively — no manual while loop or run_agent() helper needed.
  • get_gemini_client() — Lets workflows retrieve the pre-built genai.Client from inside the sandbox without os.environ access.
  • Field-level encryption — An optional SensitiveFieldsCodec encrypts credential headers (x-goog-api-key, authorization) within activity payloads using Fernet, so they never appear in plaintext in Temporal's event history. All other fields remain human-readable in the UI without a Codec Server.
    • This could arguably be a generally useful utility to provide workflow-safe http reqs directly from workflows w/o requiring users to wrap calls in activities just to make http reqs.

TODO:

  • Write tests.
    • For now I'm looking to feel out for general feedback on this approach.

More to be done to improve quality of the integration.

This currently contains a dumping ground of in progress files that will need to be cleaned up before merged into main.
Replace the previous approach (wrapping generate_content in a Temporal
activity with a manual agentic loop) with HTTP-level interception that
lets the Gemini SDK's native automatic function calling (AFC) drive the
conversation.

TemporalHttpxClient overrides httpx.AsyncClient.send() so every HTTP
call the Gemini SDK makes becomes a durable Temporal activity, recorded
in the workflow event history and subject to Temporal retry/timeout
semantics. activity_as_tool() converts @activity.defn functions into
Gemini-compatible callables dispatched via workflow.execute_activity,
making each tool invocation independently durable.

Credentials (x-goog-api-key) are stripped from serialized requests
before they reach event history and re-injected from os.environ inside
the activity. OAuth/authorization headers are intentionally left in
place since they are short-lived and cannot be reconstructed.

The package __init__.py uses lazy imports for all httpx-dependent
symbols (GeminiPlugin, TemporalHttpxClient, temporal_http_options) so
that sandbox-safe imports like activity_as_tool never trigger an httpx
load. GeminiPlugin stores the pre-built genai.Client in a passthrough'd
module so workflows can retrieve it without os.environ access.
GeminiPlugin now accepts genai.Client kwargs (api_key, vertexai, project,
etc.) directly and creates the client internally with temporal_http_options(),
eliminating the need for users to manually wire up the HTTP transport.
Activity timeout/retry configuration is exposed as explicit constructor
parameters.  Also adds TYPE_CHECKING imports for better IDE support in
__init__.py and _client_store.py.
…story

Replace the manual credential stripping/re-injection pattern (strip
x-goog-api-key before serialization, re-inject from env vars in the
activity) with a proper PayloadCodec that encrypts sensitive header
values within HttpRequestData payloads using Fernet.

This approach is better because:
- All headers (including authorization) are now protected, not just
  x-goog-api-key
- The Temporal UI still shows the full request structure with all
  non-sensitive fields readable — no Codec Server needed
- No env var dependency in the activity for credential re-injection

New module _sensitive_fields_codec.py provides three components:
- TypeTaggingPydanticJSONConverter: writes the Python type name into
  payload metadata so the codec can identify which model produced it
- SensitiveFieldsCodec: encrypts/decrypts specific keys within specific
  dict fields of registered Pydantic models
- make_sensitive_fields_data_converter: factory wiring both into a
  single DataConverter

GeminiPlugin gains two new parameters:
- sensitive_activity_fields: which header keys to encrypt (defaults to
  x-goog-api-key and authorization)
- sensitive_activity_fields_encryption_key: the Fernet key

Encryption is on by default; pass sensitive_activity_fields=None to
disable for local dev.
@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants