Add First-Class Gemini SDK Integration to Contrib#1378
Draft
JasonSteving99 wants to merge 9 commits intomainfrom
Draft
Add First-Class Gemini SDK Integration to Contrib#1378JasonSteving99 wants to merge 9 commits intomainfrom
JasonSteving99 wants to merge 9 commits intomainfrom
Conversation
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.
tconley1428
reviewed
Mar 19, 2026
tconley1428
reviewed
Mar 19, 2026
tconley1428
reviewed
Mar 19, 2026
tconley1428
reviewed
Mar 19, 2026
tconley1428
reviewed
Mar 19, 2026
tconley1428
reviewed
Mar 19, 2026
tconley1428
reviewed
Mar 19, 2026
| self, | ||
| *, | ||
| # ── Temporal activity configuration for model HTTP calls ───────── | ||
| start_to_close_timeout: timedelta = timedelta(seconds=60), |
Contributor
There was a problem hiding this comment.
We should take an ActivityConfig, allow customization of all options.
tconley1428
reviewed
Mar 19, 2026
|
|
||
| def _configure_data_converter( | ||
| self, converter: DataConverter | None | ||
| ) -> DataConverter: |
Contributor
There was a problem hiding this comment.
This overrides failureconverter as well. I would just a pattern more similar to existing plugins.
Introduce TemporalApiClient, a BaseApiClient subclass that routes async_request() through Temporal activities instead of intercepting at the httpx transport layer. The real genai.Client with real credentials only exists inside the activity on the worker side. This solves two problems with the previous approach: - Credentials (bearer tokens, API keys) no longer appear in activity inputs or Temporal event history - No credential fetching/refreshing happens in the workflow, eliminating non-deterministic network calls The SDK's AFC loop runs in the workflow naturally, with each API call dispatched as an activity and each tool invocation (via activity_as_tool) also running as an activity. Key changes: - Add _temporal_api_client.py (TemporalApiClient, request/response models) - Add _gemini_activity.py (GeminiApiCaller with cached client) - Update gemini_client() to return AsyncClient directly - Update activity_as_tool() to accept ActivityConfig - Simplify GeminiPlugin (no more sensitive fields codec) - Remove _temporal_httpx_client.py, _http_activity.py, _sensitive_fields_codec.py
- Add _SerializableHttpOptions model for forwarding per-request options (base_url, api_version, headers, extra_body) across the activity boundary. Non-serializable fields (httpx_client, etc.) are rejected with a clear error pointing to GeminiPlugin init. - Map per-request timeout to activity start_to_close_timeout since Temporal owns timeouts/retries, not the underlying HTTP client. - Add activity summaries: API path for gemini_api_call, "tool_call" for activity_as_tool invocations. - Simplify activity_as_tool to accept ActivityConfig instead of individual timeout/retry/cancellation fields. - Override close/aclose/__del__ on TemporalApiClient to prevent cleanup errors from the SDK accessing nonexistent HTTP resources. - Remove unused GeminiAgentWorkflowError and GeminiToolSerializationError. - Rename activity from gemini_sdk_api_call to gemini_api_call.
GeminiPlugin and GeminiApiCaller now accept a caller-provided genai.Client instead of constructing one internally from kwargs, following standard dependency injection. Adds 21 tests covering generate_content, AFC tool calling (single-arg, multi-arg, workflow method tools), tool failure propagation, sequential multi-tool calls, and end-to-end http_options propagation. Tests use a tracking gemini_api_call activity injected via monkey-patching GeminiApiCaller.activity, exercising the real plugin's data converter, sandbox passthrough, and workflow runner configuration.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 thegenai.Client, registers the HTTP transport activity, configures the Pydantic data converter, and sets up sandbox passthrough modules. Users pass the samekwargsthey'd pass togenai.Client().httpx.AsyncClient(TemporalHttpxClient) intercepts every HTTP request the Gemini SDK makes and dispatches it as agemini_api_callactivity viaworkflow.execute_activity. This makes all model calls (including streaming) durable and visible in event history.activity_as_tool()— Converts any@activity.defnfunction into a Gemini-compatible tool callable. Gemini's automatic function calling (AFC) drives the agentic loop natively — no manualwhileloop orrun_agent()helper needed.get_gemini_client()— Lets workflows retrieve the pre-builtgenai.Clientfrom inside the sandbox withoutos.environaccess.SensitiveFieldsCodecencrypts credential headers (x-goog-api-key,authorization) within activity payloads usingFernet, so they never appear in plaintext in Temporal's event history. All other fields remain human-readable in the UI without a Codec Server.TODO: