google-genai client in place and instruments:
client.models.generate_content(...)/generateContentclient.models.generate_content_stream(...)/generateContentStream- the async equivalents on
client.aio.models(Python)
Only the unified SDK is supported. The legacy
google-generativeai package (genai.GenerativeModel(...) in Python, GoogleGenerativeAI in JavaScript) has a different surface that cannot be instrumented, so wrap() rejects it with a migration message rather than silently wrapping nothing — you will know immediately, not at invoice time.Migrate to google-genai / @google/genai. See Google’s migration guide.Install
Wrap and call
Client object, not a per-model handle. The SDK installs its wrappers on client.models (and client.aio.models in Python).
Streaming
Usage lives on the final chunk’susage_metadata / usageMetadata. The SDK wraps the iterator and emits once it is exhausted.
The JavaScript wrapper reads both camelCase (
usageMetadata) and snake_case (usage_metadata) forms, since the transport varies across @google/genai versions.Async
Python exposes the async surface underclient.aio.models. The SDK instruments it at wrap time, so nothing extra is needed.
Python
@google/genai returns plain promises, so the wrapper simply awaits and emits. No proxy machinery involved.
Per-call override
extra_lago / lago before forwarding, so Google’s request validation never sees it.
What gets captured
Reasoning tokens populate automatically on Gemini 2.5. The model reasons internally by default, so
thoughts_token_count shows up without you enabling anything, and it is money you are already paying Google.cache_read, audio_input, and image_input are breakdowns inside input, not additions to it — Google’s docs are explicit that prompt_token_count includes cached content. Bill in tokens shows the metric setup that handles this.
Unrecognized top-level usage fields land in CanonicalUsage.extras, which is how provider drift becomes visible instead of silently dropped.
Pricing
In price mode, Gemini models are priced from OpenRouter’s public model list, per token, refreshed hourly on the background thread.What is and isn’t instrumented
wrap() patches generate_content and generate_content_stream on client.models, and the same two on client.aio.models.
Chat sessions are covered. client.chats and client.aio.chats hold a reference to the same models object the SDK patched, so chat.send_message(...) routes through the instrumented call and bills normally. Create the chat after wrap().
Other model operations are not. embed_content, generate_images, generate_videos, and the image editing calls consume billable Google quota and emit no Lago event, as do client.batches and client.caches. If you bill on those, build a CanonicalUsage yourself and pass it to sdk.emit().
Audio and image tokens are captured but not priced separately. They are reported as llm_audio_input_tokens, llm_audio_output_tokens, and llm_image_input_tokens, and in price mode they are billed at the model’s text rate. See Known limits.
Next steps
Configuration reference
Every config knob, in both SDKs.
Bill in dollars
Emit cost per call instead of token counts.