Google Gen AI
Trace Gemini and Vertex AI generation, embedding, function-call, media, and stream operations from TypeScript or Python.
Wrap your Google Gen AI client, and each model call you make with it becomes a span in telemetry.dev. This works for the Gemini Developer API and for Vertex AI. Your calls and their return values don’t change.
What you get
- Contents, system instructions, model, sampling parameters, tools, tool configuration, safety settings, and response format
- Response IDs, model versions, candidate content, finish reasons, safety data, errors, and time to first chunk
- Input, output, total, cache-read, reasoning, and tool-use prompt token counts, plus text, image, and audio token counts as
gen_ai.usage.{text,image,audio}.{input_tokens,output_tokens,cache_read.input_tokens} - Embedding counts, dimensions, input token counts, and billable characters
- Image generation, image edit, image upscale, and video generation calls
- One span for a whole automatic function-calling loop, with the usage of every model call in it
The integration never records embedding vectors or image, video, and reference-image bytes.
Install
npm i @telemetry-dev/sdk @telemetry-dev/google-genai @google/genaipip install telemetry-dev-google-genaiThe Python package also installs telemetry-dev and google-genai.
| Language | Requirements |
|---|---|
| TypeScript | Node.js >=20.19.0, @telemetry-dev/sdk ^0.1.8, @google/genai >=2 <3 |
| Python | Python >=3.10, telemetry-dev >=0.2.6, google-genai >=2,<3 |
Trace a call
Set your telemetry.dev project API key and your Gemini key. Copy the telemetry.dev key from the project setup page; it starts with td_live_.
export TELEMETRY_DEV_API_KEY=td_live_...
export GEMINI_API_KEY=...
Call init(), wrap the client, and use it as usual:
import { GoogleGenAI } from "@google/genai";
import { flush, init, shutdown } from "@telemetry-dev/sdk";
import { wrapGoogleGenAI } from "@telemetry-dev/google-genai";
init({ serviceName: "google-genai-app" });
const ai = wrapGoogleGenAI(
new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY! }),
);
try {
const response = await ai.models.generateContent({
model: "gemini-2.5-flash",
contents: "Tell me a joke about OpenTelemetry",
config: { systemInstruction: "You are a helpful bot" },
});
console.log(response.text ?? "(no content)");
} finally {
await flush();
await shutdown();
}import os
import telemetry_dev
from google import genai
from telemetry_dev_google_genai import wrap_google_genai
telemetry_dev.init(service_name="google-genai-app")
client = wrap_google_genai(
genai.Client(api_key=os.environ["GEMINI_API_KEY"]),
)
try:
response = client.models.generate_content(
model="gemini-2.5-flash",
contents="Tell me a joke about OpenTelemetry",
config={"system_instruction": "You are a helpful bot"},
)
print(response.text or "(no content)")
finally:
telemetry_dev.flush()
telemetry_dev.shutdown()Run the script, then open the trace explorer. The trace has one chat gemini-2.5-flash generation span with the contents, the reply, and token usage.
Without an API key, init() turns telemetry off and the wrapped client works as before.
Instrument all clients
Python can trace every client without wrapping each one. Call the global function once at startup:
from telemetry_dev_google_genai import instrument_google_genai
instrument_google_genai()
TypeScript has no global instrumentation. In @google/genai 2.x, model methods are instance fields rather than prototype methods, so wrap each client with wrapGoogleGenAI().
API
| TypeScript | Python | What it does |
|---|---|---|
wrapGoogleGenAI(client) |
wrap_google_genai(client) |
Instruments one client and returns it. Wrapping the same client again has no effect. TypeScript covers client.models and video operation polls. Python covers client.models, client.aio.models, and operations.get(). |
| None | instrument_google_genai() |
Instruments every client through the synchronous and asynchronous model classes. |
| None | uninstrument_google_genai() |
Removes global instrumentation. |
These functions take no options.
Traced calls
The integration traces synchronous and asynchronous calls.
| Google Gen AI call | Span name | Span type |
|---|---|---|
models.generateContent() (generate_content() in Python) |
chat {model} |
generation |
models.generateContentStream() (generate_content_stream()) |
chat {model} |
generation |
models.embedContent() (embed_content()) |
embeddings {model} |
embedding |
models.generateImages(), editImage(), and upscaleImage() |
generate_content {model} |
generation |
models.generateVideos() (generate_videos()) |
generate_content {model} |
generation |
Chats from client.chats.create() call these model methods, so sendMessage() and sendMessageStream() (send_message() and send_message_stream() in Python) are traced too, whether you create the chat before or after wrapping.
The integration doesn’t trace countTokens, computeTokens, live, caches, files, tunings, or batches.
Images and video
Image and video spans use the operation generate_content and set gen_ai.output.type to image or video. They keep the model, the text prompt, non-binary configuration, input metadata such as URIs and MIME types, the output count, and the operation ID and storage URIs when the response has them.
generateVideos() returns a long-running operation, and the two languages trace it differently.
TypeScript
The submission is one span that ends when the call returns. Each later client.operations.get() or getVideosOperation() call starts its own generate_content video operation span. This works only when you pass the operation object that the same wrapped client returned; a rebuilt operation object with the same name isn’t traced. The last poll records the final output metadata when it is available. No span stays open across the polling loop.
Python
If generate_videos() returns an unfinished operation, its span stays open. It finishes when operations.get() on the same wrapped client sees the operation reach a terminal state. A poll from another client or process can’t finish it.
The span ends with an error instead of staying open in these cases:
- The integration’s operation tracker runs out of room and drops the operation.
- You close the client:
client.close()ends pending spans for synchronous operations, andawait client.aio.aclose()ends them for asynchronous operations. Closing one doesn’t affect the other. - You call
uninstrument_google_genai(). This ends spans tracked byinstrument_google_genai()only, not those tracked bywrap_google_genai().
Automatic function calling
When the Google SDK runs your tools itself, one call produces one span for the whole loop, and the span’s usage is the sum of every model call in it. The span input becomes the SDK’s automatic function-calling history, and the span sets google_genai.automatic_function_calling to true.
In Python, pass Python functions in config.tools:
import os
from google import genai
from telemetry_dev_google_genai import wrap_google_genai
def get_weather(location: str) -> str:
return f"rainy, 57°F in {location}"
client = wrap_google_genai(genai.Client(api_key=os.environ["GEMINI_API_KEY"]))
response = client.models.generate_content(
model="gemini-2.5-flash",
contents="Weather in Paris?",
config={"tools": [get_weather]},
)
print(response.text or "(no content)")
In TypeScript, the loop runs only for callable tools, which have a callTool method. Plain functionDeclarations don’t start a loop, so each generateContent() call you make gets its own span. The usage sum needs AsyncLocalStorage. On an edge runtime without it, the call is still traced, but without the summed usage.
Streaming
A stream span stays open until your code reads the stream to the end, stops early, or the stream fails. A stream you never read never finishes its span. Chunks reach your code unchanged.
Gemini stream chunks already carry cumulative usage, so the integration doesn’t change the request. The span combines text parts, thinking parts, candidates, finish reasons, usage, and automatic function-calling turns.
In TypeScript, the wrapper keeps the stream’s return, throw, and Symbol.asyncDispose behavior. In Python, call close() when you stop a synchronous stream early, or aclose() for an asynchronous one.
Provider names
| Client | Provider |
|---|---|
| Gemini Developer API | gcp.gemini |
Vertex AI (vertexai: true, or vertexai=True in Python) |
gcp.vertex_ai |
Captured content
Prompts and responses follow the core SDK’s captureInput, captureOutput, and mask settings (capture_input, capture_output, and mask in Python). With output capture off, response IDs, models, finish reasons, usage, timing, and errors are still recorded.
Some data is stored outside those settings:
- Tool definitions (
gen_ai.tool.definitions), including descriptions and parameter schemas, are request attributes. Turning off input capture or setting a mask doesn’t remove them. - In the current releases (TypeScript 0.1.9, Python 0.1.6), the block reason message, safety ratings, grounding metadata, and URL-context metadata are stored as attributes even when output capture is off, and the mask doesn’t see them. Grounding metadata can repeat the answer text and include retrieved document text, source titles and URIs, and search queries. An unreleased change keeps only their structure, such as categories, scores, and offsets, unless output is captured and no mask is set.
- Stop sequences, metadata and attributes you pass yourself, and exception messages aren’t covered by the capture settings.
Redact these separately if they can hold sensitive data. See privacy for server-side redaction.
Send telemetry before exit
In a short script, call flush() and then shutdown() before the process exits, as in the example above. In serverless code, call flush() before the handler returns. In TypeScript you can pass a waitUntil function to init() instead. See the TypeScript SDK and Python SDK guides for details.