Skip to content

Commit f0fd4a8

Browse files
Sailesh Ramakrishnanclaude
andcommitted
Add Google Gemini adapter
Wraps google-genai Client.models.generate_content to record tokens (prompt/candidates/cached), tool-call names from candidates[].content .parts[].function_call.name, model, and latency. Provider string is "google" to match existing gemini-2.5-* entries in the cost registry. Installed via the new [gemini] extra (pip install aimeter[gemini]). Mirrors the Anthropic adapter structure and test coverage. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 09473d9 commit f0fd4a8

4 files changed

Lines changed: 422 additions & 1 deletion

File tree

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@ dependencies = []
2727
[project.optional-dependencies]
2828
openai = ["openai>=1.0"]
2929
anthropic = ["anthropic>=0.30"]
30+
gemini = ["google-genai>=0.3"]
3031
langchain = ["langchain-core>=0.2"]
31-
all = ["aimeter[openai,anthropic,langchain]"]
32+
all = ["aimeter[openai,anthropic,gemini,langchain]"]
3233
dev = [
3334
"pytest>=8.0",
3435
"pytest-asyncio>=0.23",

src/aimeter/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
_LAZY_IMPORTS = {
3131
"track_openai": ("aimeter.adapters.openai", "track_openai"),
3232
"track_anthropic": ("aimeter.adapters.anthropic", "track_anthropic"),
33+
"track_gemini": ("aimeter.adapters.gemini", "track_gemini"),
3334
}
3435

3536

@@ -68,6 +69,7 @@ def __getattr__(name: str):
6869
# Adapters
6970
"track_openai",
7071
"track_anthropic",
72+
"track_gemini",
7173
"track_llm_call",
7274
# Outcome
7375
"record_outcome",

src/aimeter/adapters/gemini.py

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
"""Google Gemini adapter — thin wrapper for cost tracking.
2+
3+
Wraps a google-genai client to automatically track token usage and costs
4+
for all generate_content calls.
5+
6+
Usage:
7+
from aimeter import track_gemini
8+
from google import genai
9+
10+
client = track_gemini(genai.Client(api_key="..."), project="my-agent")
11+
response = client.models.generate_content(
12+
model="gemini-2.5-flash",
13+
contents="Hello",
14+
)
15+
# Tokens, cost, and latency are automatically tracked.
16+
"""
17+
18+
from __future__ import annotations
19+
20+
import time
21+
from typing import Any
22+
23+
from aimeter.tracker import get_tracker
24+
from aimeter.types import LLMEvent, TokenUsage
25+
26+
27+
def _extract_usage(response: Any) -> TokenUsage:
28+
"""Extract token usage from a google-genai response object."""
29+
usage = getattr(response, "usage_metadata", None)
30+
if usage is None:
31+
return TokenUsage()
32+
33+
return TokenUsage(
34+
input_tokens=getattr(usage, "prompt_token_count", 0) or 0,
35+
output_tokens=getattr(usage, "candidates_token_count", 0) or 0,
36+
cached_tokens=getattr(usage, "cached_content_token_count", 0) or 0,
37+
)
38+
39+
40+
def _extract_tool_calls(response: Any) -> list[str]:
41+
"""Extract function call names from a google-genai response (names only, for privacy)."""
42+
try:
43+
candidates = getattr(response, "candidates", None) or []
44+
names: list[str] = []
45+
for candidate in candidates:
46+
content = getattr(candidate, "content", None)
47+
if content is None:
48+
continue
49+
parts = getattr(content, "parts", None) or []
50+
for part in parts:
51+
function_call = getattr(part, "function_call", None)
52+
if function_call is None:
53+
continue
54+
name = getattr(function_call, "name", None)
55+
if name:
56+
names.append(name)
57+
return names
58+
except (TypeError, AttributeError):
59+
return []
60+
61+
62+
def _extract_model(response: Any, kwargs: dict[str, Any]) -> str:
63+
"""Get model name from response (preferred) or request kwargs."""
64+
return (
65+
getattr(response, "model_version", None)
66+
or getattr(response, "model", None)
67+
or kwargs.get("model", "")
68+
)
69+
70+
71+
class _TrackedModels:
72+
"""Proxy for client.models that tracks generate_content() calls."""
73+
74+
def __init__(self, models: Any, project: str, tags: dict[str, str],
75+
run_id: str) -> None:
76+
self._models = models
77+
self._project = project
78+
self._tags = tags
79+
self._run_id = run_id
80+
81+
def generate_content(self, **kwargs: Any) -> Any:
82+
"""Call models.generate_content and record the event."""
83+
start = time.perf_counter_ns()
84+
error = None
85+
86+
try:
87+
response = self._models.generate_content(**kwargs)
88+
except Exception as exc:
89+
error = str(exc)
90+
latency_ms = (time.perf_counter_ns() - start) / 1_000_000
91+
event = LLMEvent(
92+
run_id=self._run_id,
93+
project=self._project,
94+
provider="google",
95+
model=kwargs.get("model", ""),
96+
event_type="llm.call",
97+
tokens=TokenUsage(),
98+
latency_ms=latency_ms,
99+
error=error,
100+
tags=dict(self._tags),
101+
)
102+
get_tracker().record(event)
103+
raise
104+
105+
latency_ms = (time.perf_counter_ns() - start) / 1_000_000
106+
event = LLMEvent(
107+
run_id=self._run_id,
108+
project=self._project,
109+
provider="google",
110+
model=_extract_model(response, kwargs),
111+
event_type="llm.call",
112+
tokens=_extract_usage(response),
113+
latency_ms=latency_ms,
114+
tool_calls=_extract_tool_calls(response),
115+
tags=dict(self._tags),
116+
)
117+
get_tracker().record(event)
118+
return response
119+
120+
def __getattr__(self, name: str) -> Any:
121+
return getattr(self._models, name)
122+
123+
124+
class _TrackedGemini:
125+
"""Proxy around a google-genai client that tracks all LLM calls."""
126+
127+
def __init__(self, client: Any, project: str, tags: dict[str, str],
128+
run_id: str) -> None:
129+
self._client = client
130+
self.models = _TrackedModels(client.models, project, tags, run_id)
131+
132+
def __getattr__(self, name: str) -> Any:
133+
return getattr(self._client, name)
134+
135+
136+
def track_gemini(
137+
client: Any,
138+
*,
139+
project: str = "default",
140+
tags: dict[str, str] | None = None,
141+
run_id: str = "",
142+
) -> Any:
143+
"""Wrap a google-genai client to automatically track costs.
144+
145+
Args:
146+
client: A google.genai.Client() instance.
147+
project: Project name for grouping events.
148+
tags: Optional tags for filtering/grouping.
149+
run_id: Optional run ID for grouping multiple calls.
150+
151+
Returns:
152+
A wrapped client that behaves identically but tracks all LLM calls.
153+
154+
Example:
155+
from google import genai
156+
from aimeter import track_gemini
157+
158+
client = track_gemini(genai.Client(api_key="..."), project="my-agent")
159+
response = client.models.generate_content(
160+
model="gemini-2.5-flash",
161+
contents="Hello",
162+
)
163+
# Cost and tokens are automatically tracked.
164+
"""
165+
return _TrackedGemini(client, project, tags or {}, run_id)

0 commit comments

Comments
 (0)