Skip to content
telemetry.dev
Esc
↑↓navigate↵open⌘Jpreview
On this page

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/genai
pip install telemetry-dev-google-genai

The 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, and await client.aio.aclose() ends them for asynchronous operations. Closing one doesn’t affect the other.
  • You call uninstrument_google_genai(). This ends spans tracked by instrument_google_genai() only, not those tracked by wrap_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.

Last updated on October 18, 2026

Was this page helpful?