Skip to content

feat(closes OPEN-11891): add google-genai tracer with Vertex support - #660

Open
viniciusdsmello wants to merge 2 commits into
mainfrom
vini/open-11891-python-sdk-trace_gemini-only-supports-the-legacy-google
Open

feat(closes OPEN-11891): add google-genai tracer with Vertex support#660
viniciusdsmello wants to merge 2 commits into
mainfrom
vini/open-11891-python-sdk-trace_gemini-only-supports-the-legacy-google

Conversation

@viniciusdsmello

Copy link
Copy Markdown
Contributor

Closes OPEN-11891.

trace_gemini targeted only the legacy google-generativeai SDK. Users on the current unified SDK (google.genai.Client, including vertexai=True — what Google Cloud customers use) got no auto-instrumentation at all, silently: the registry probed google.generativeai, failed _is_installed, and skipped behind a logger.debug. Explicit trace_gemini(client) raised. Reported from a Vertex AI customer PoC that hand-rolled chat_completion spans as a workaround.

What's covered

Surface
client.models.generate_content sync
client.models.generate_content_stream sync, Iterator
client.aio.models.generate_content coroutine
client.aio.models.generate_content_stream coroutine returning AsyncIteratorawait, then async for
client.chats.create(...).send_message(...) free — Chats(modules=client.models) shares the patched Models instance

trace_gemini now sniffs the client type and dispatches, importing only the SDK it was handed, so the issue's repro works unchanged. trace_google_genai is exported for explicitness.

Three things worth a reviewer's attention

1. Model-name normalization is the biggest fix here, bigger than the tracer. Cost lookup is an exact lowercased (provider, model) match with no aliasing, and cost rows are keyed on bare slugs. Verified by publishing rows and reading openlayer_cost back:

(gemini, models/gemini-2.5-flash)  ->  $0.00
(gemini, gemini-2.5-flash)         ->  $2.80     # identical tokens

The API accepts prefixed names and Vertex hands out full resource paths (projects/…/publishers/google/models/…), so this was a live silent-$0 path. _normalize_model_name takes the last / segment. Version suffixes are left intact deliberately — upstream coverage for them is inconsistent (gemini-2.0-flash-001 prices, gemini-2.5-flash-001 doesn't, under every slug) and rewriting them would misreport which model ran.

2. Token accounting was wrong by ~10x on thinking models. candidates_token_count excludes thoughts_token_count, and thinking is on by default for 2.5-series. A live call measured:

prompt 11
candidates 29
thoughts 342
total 382
old prompt + candidates 40

So tokens now uses total_token_count, and completion_tokens = candidates + thoughts because Vertex bills thinking as output and the backend prices from completionTokens. The raw split goes to metadata. Streaming caveat: usage_metadata is on every chunk and cumulative — last-chunk-wins, summing would multiply-count. Same bug fixed in gemini_tracer.

3. ADK would have double-billed without the suppression. google.adk…Gemini.api_client is a google.genai.Client, and generate_content_async calls self.api_client.aio.models.generate_content. Since google-adk depends on google-genai, a google.genai registry entry activates for every ADK user, and each ADK LLM call would emit two spans and two costs. The tracer stands down when the ADK tracer already has an LLM step open, checked via sys.modules (no import — google_adk_tracer is 66 KB and a guarded lazy import would build an ImportError on every call in the common case). There's a real Runner + LlmAgent test asserting exactly one span with both tracers active, plus a control proving the check is contextual rather than "is ADK importable".

provider = "gemini"

Both Gemini tracers now emit the real cost slug rather than the "Google" display label. Measured, by publishing rows and reading cost back rather than counting the feed:

  • "Google" prices 7 of 9 current models correctly — it is not broadly broken, and an earlier draft of this change overstated the gap.
  • gemini-3-pro-preview resolves only under gemini ($14.00 vs $0.00).
  • Amounts are not identical where both resolve. Of 35 shared gemini-*/gemma-* models, 6 disagree — gemini-flash-latest is 3.4x ($0.00525 under Google vs $0.00155 under gemini). gemini's figure matches gemini-2.5-flash pricing exactly; the Google row looks like stale pro-tier pricing.

So the switch buys accuracy and future-proofing (google rows come from OpenRouter's catalog, which lags Google's newest and Vertex-only models; gemini rows come from LiteLLM). It does move reported dollar amounts for existing legacy-SDK users on the -latest aliases — happy to split gemini_tracer.py out into its own change if you'd rather not carry that here.

google_adk_tracer is deliberately left on "Google". BaseLlmFlow._call_llm_async resolves the LLM via __get_llm() and is model-agnostic, so LiteLlm and Claude-on-Vertex agents hit the same wrapper — "gemini" there would pair the slug with a claude-* model and miss the lookup entirely.

Verification

  • 57 tests, no network. Model shapes, token totals, sync/async × streaming/non-streaming, chats, Vertex vs AI Studio metadata, ADK dedup, idempotency, dispatch, registry.
  • No regressions, compared against a pristine origin/main worktree at the same filesystem path. (Comparing across paths is misleading — a stray .env at the checkout root leaks a real key into test_tracer_configuration.py; those failures and the xdist-order flakiness in test_claude_agent_sdk / test_transform reproduce identically on origin/main.)
  • End-to-end on live AI Studio — non-streaming, streaming, async streaming, chats, and prefixed model names, all publishing rows with cost resolving and provider=gemini, model=gemini-2.5-flash.

Known gap

Vertex mode has no live verification. The available credential was an AI Studio key, so llm_system: google_vertex, the resource-path model shapes, and gcp_project / gcp_location extraction are unit-tested only. Those last two read client._api_client, private API that a google-genai bump could break with nothing in CI to catch it. A reviewer with a GCP project could close this.

Follow-ups worth filing

  • Version-suffixed model IDs (gemini-2.5-flash-001) are unpriced under every slug — a server-side suffix fallback would fix all SDKs at once.
  • google_adk_tracer sets model_name = llm_request.model with no normalization, so ADK users configuring full resource paths price at $0 regardless of slug.
  • provider does double duty as UI label and exact cost key, which is the root of OPEN-9928 / OPEN-11695 / OPEN-11901. A server-side alias map would let SDKs emit friendly labels.
  • OPEN-11903 (TS parity) should copy the slug decision and token rules from here rather than re-deriving them.

🤖 Generated with Claude Code

https://claude.ai/code/session_01A8GauFiK5edFUfGnbjJrtz

viniciusdsmello and others added 2 commits July 28, 2026 16:43
trace_gemini targeted only the legacy google-generativeai SDK, so users of
the unified google.genai.Client -- including Vertex mode, which is what
Google Cloud customers use -- got no auto-instrumentation at all
(auto_instrument() returned {"gemini": False} behind a logger.debug) and a
hard ValueError on an explicit trace_gemini(client) call.

Adds google_genai_tracer covering client.models.generate_content and
generate_content_stream, their async equivalents under client.aio.models,
and chat sessions via client.chats -- the last for free, since
Chats(modules=client.models) shares the patched Models instance.

trace_gemini now sniffs the client type and dispatches, importing only the
SDK it was handed, so the issue's repro works unchanged. trace_google_genai
is exported for explicitness.

Model names reduce to the bare slug (last path segment), covering models/...
and full Vertex resource paths. Verified load-bearing against the live API:
models/gemini-2.5-flash prices at $0.00 where bare gemini-2.5-flash prices
at $2.80 on identical tokens.

Tokens use total_token_count with thinking tokens folded into
completion_tokens. candidates_token_count excludes thoughts and thinking is
on by default for 2.5-series models, so prompt+candidates undercounts badly
-- a live call measured 382 real tokens (11 prompt + 29 candidates + 342
thoughts) where the old formula reported 40. Also fixed in gemini_tracer,
which shared the bug, along with its async streaming path: `async for i,
chunk in enumerate(chunks)` raises, since enumerate returns a plain
iterator, so that path could never have run.

provider is "gemini" for both Gemini tracers -- a real cost-table slug.
Lookup is an exact lowercased (provider, model) match with no aliasing, and
"Google" prices gemini-3-pro-preview at $0 and carries stale
OpenRouter-sourced prices for the -latest aliases. google_adk_tracer is
deliberately left on "Google": BaseLlmFlow._call_llm_async is
model-agnostic, so LiteLlm and Claude-on-Vertex agents reach the same
wrapper and would be paired with a mismatched slug.

Suppresses its own span when the ADK tracer already has an LLM step open.
ADK's api_client IS a genai.Client calling aio.models.generate_content, so
both integrations activate for ADK users and would otherwise double-count
spans and cost.

57 tests, no network, including a real Runner + LlmAgent turn asserting
exactly one chat_completion span with both tracers active. Verified against
a pristine origin/main worktree at the same path: no regressions.
End-to-end verified on live AI Studio (non-streaming, streaming, async
streaming, chats, prefixed model names) with cost resolving on every path.
Vertex mode is unit-tested only -- no Vertex credentials available.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A8GauFiK5edFUfGnbjJrtz
CI's lint env has neither google-genai nor google-adk installed, so
`from google import genai` resolves to an unknown attribute on the
`google` namespace package (reportMissingTypeStubs +
reportAttributeAccessIssue). Locally, where the package IS installed,
pyright instead flags `inference_id` as an undeclared kwarg on
generate_content -- which is the point, since the tracer pops it before
calling through.

Suppress those three at file level in both new test files, and fix a
genuine typing slip: the async streaming comprehension yielded
list[str | None] against a List[str] annotation.

Verified against a venv with no google-* packages (matching CI) as well
as the local env with both installed: 0 errors either way, and no
pyright errors attributable to the new files repo-wide.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A8GauFiK5edFUfGnbjJrtz
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.

1 participant