Skip to content

Commit 9a00e0b

Browse files
authored
Handle bytes in google genai messages (#1231)
1 parent 938c212 commit 9a00e0b

File tree

2 files changed

+35
-11
lines changed

2 files changed

+35
-11
lines changed

logfire/_internal/integrations/google_genai.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
from __future__ import annotations
22

3+
import base64
34
import json
45
from typing import Any
56

67
from opentelemetry._events import Event, EventLogger, EventLoggerProvider
78
from opentelemetry.instrumentation.google_genai import GoogleGenAiSdkInstrumentor
89
from opentelemetry.trace import get_current_span
10+
from typing_extensions import TypeAlias
911

1012
import logfire
1113
from logfire._internal.utils import handle_internal_errors
1214

15+
Part: TypeAlias = 'dict[str, Any] | str'
16+
17+
18+
def default_json(x: Any) -> str:
19+
return base64.b64encode(x).decode('utf-8') if isinstance(x, bytes) else x
20+
1321

1422
class SpanEventLogger(EventLogger):
1523
@handle_internal_errors
@@ -18,18 +26,25 @@ def emit(self, event: Event) -> None:
1826
assert isinstance(event.body, dict)
1927
body: dict[str, Any] = {**event.body}
2028
if event.name == 'gen_ai.choice':
21-
parts = body.pop('content')['parts']
22-
new_parts: list[dict[str, Any] | str] = []
23-
for part in parts:
24-
new_part: str | dict[str, Any] = {k: v for k, v in part.items() if v is not None}
25-
if list(new_part.keys()) == ['text']: # pragma: no branch
26-
new_part = new_part['text']
27-
new_parts.append(new_part)
28-
body['message'] = {'role': 'assistant', 'content': new_parts}
29+
if 'content' in body: # pragma: no branch
30+
parts = body.pop('content')['parts']
31+
new_parts = [transform_part(part) for part in parts]
32+
body['message'] = {'role': 'assistant', 'content': new_parts}
2933
else:
34+
if 'content' in body: # pragma: no branch
35+
body['content'] = transform_part(body['content'])
3036
body['role'] = body.get('role', event.name.split('.')[1])
3137

32-
span.add_event(event.name, attributes={'event_body': json.dumps(body)})
38+
span.add_event(event.name, attributes={'event_body': json.dumps(body, default=default_json)})
39+
40+
41+
def transform_part(part: Part) -> Part:
42+
if isinstance(part, str):
43+
return part
44+
new_part = {k: v for k, v in part.items() if v is not None}
45+
if list(new_part.keys()) == ['text']:
46+
return new_part['text']
47+
return new_part
3348

3449

3550
class SpanEventLoggerProvider(EventLoggerProvider):

tests/otel_integrations/test_google_genai.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@ def get_current_weather(location: str) -> str:
4343

4444
response = client.models.generate_content( # type: ignore
4545
model='gemini-2.0-flash-001',
46-
contents='What is the weather like in Boston?',
46+
contents=[
47+
'What is the weather like in Boston?',
48+
types.Part.from_bytes(data=b'123', mime_type='text/plain'),
49+
],
4750
config=types.GenerateContentConfig(
4851
tools=[get_current_weather],
4952
),
@@ -57,7 +60,7 @@ def get_current_weather(location: str) -> str:
5760
'context': {'trace_id': 1, 'span_id': 1, 'is_remote': False},
5861
'parent': None,
5962
'start_time': IsInt(),
60-
'end_time': 3000000000,
63+
'end_time': 4000000000,
6164
'attributes': {
6265
'code.function.name': 'google.genai.Models.generate_content',
6366
'gen_ai.system': 'gemini',
@@ -71,6 +74,12 @@ def get_current_weather(location: str) -> str:
7174
'logfire.metrics': IsPartialDict(),
7275
'events': [
7376
{'content': 'What is the weather like in Boston?', 'role': 'user'},
77+
{
78+
'content': {
79+
'inline_data': {'display_name': None, 'data': 'MTIz', 'mime_type': 'text/plain'}
80+
},
81+
'role': 'user',
82+
},
7483
{
7584
'index': 0,
7685
'finish_reason': 'STOP',

0 commit comments

Comments
 (0)