Skip to content

Commit 3f10d20

Browse files
author
Davi Campos
committed
fix(google-genai): prevent TypeError when handling Pydantic model classes
1 parent bd3c1f2 commit 3f10d20

File tree

5 files changed

+60
-3
lines changed

5 files changed

+60
-3
lines changed

instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/dict_util.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,14 +188,22 @@ def _flatten_compound_value(
188188
rename_keys=rename_keys,
189189
flatten_functions=flatten_functions,
190190
)
191-
if hasattr(value, "model_dump"):
191+
if hasattr(value, "model_dump") and not isinstance(value, type):
192192
return _flatten_dict(
193193
value.model_dump(),
194194
key_prefix=key,
195195
exclude_keys=exclude_keys,
196196
rename_keys=rename_keys,
197197
flatten_functions=flatten_functions,
198198
)
199+
if hasattr(value, "model_json_schema"):
200+
return _flatten_dict(
201+
value.model_json_schema(),
202+
key_prefix=key,
203+
exclude_keys=exclude_keys,
204+
rename_keys=rename_keys,
205+
flatten_functions=flatten_functions,
206+
)
199207
return _flatten_compound_value_using_json(
200208
key,
201209
value,

instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/generate_content.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,10 @@ def _determine_genai_system(models_object: Union[Models, AsyncModels]):
161161
def _to_dict(value: object):
162162
if isinstance(value, dict):
163163
return value
164-
if hasattr(value, "model_dump"):
164+
if hasattr(value, "model_dump") and not isinstance(value, type):
165165
return value.model_dump()
166+
if hasattr(value, "model_json_schema"):
167+
return value.model_json_schema()
166168
return json.loads(json.dumps(value))
167169

168170

instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/tool_call_wrapper.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,12 @@ def _to_otel_value(python_value):
5454
return {
5555
key: _to_otel_value(val) for (key, val) in python_value.items()
5656
}
57-
if hasattr(python_value, "model_dump"):
57+
if hasattr(python_value, "model_dump") and not isinstance(
58+
python_value, type
59+
):
5860
return python_value.model_dump()
61+
if hasattr(python_value, "model_json_schema"):
62+
return python_value.model_json_schema()
5963
if hasattr(python_value, "__dict__"):
6064
return _to_otel_value(python_value.__dict__)
6165
return repr(python_value)

instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/utils/test_dict_util.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,25 @@ def test_flatten_with_mixed_structures():
207207
}
208208

209209

210+
def test_flatten_with_pydantic_class_not_instance():
211+
"""Test that passing a Pydantic class (not instance) doesn't cause model_dump() error."""
212+
input_dict = {
213+
"foo": PydanticModel, # Class, not instance
214+
"bar": "value",
215+
}
216+
217+
# Should not raise "BaseModel.model_dump() missing 1 required positional argument: 'self'"
218+
output = dict_util.flatten_dict(input_dict)
219+
# The class should be converted using JSON serialization fallback
220+
assert "bar" in output
221+
assert output["bar"] == "value"
222+
assert "foo.description" in output
223+
assert "foo.properties.int_value.title" in output
224+
assert "foo.properties.int_value.default" in output
225+
assert "foo.properties.str_value.title" in output
226+
assert "foo.properties.str_value.default" in output
227+
228+
210229
def test_converts_tuple_with_json_fallback():
211230
input_dict = {
212231
"foo": ("abc", 123),

instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/utils/test_tool_call_wrapper.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from unittest.mock import patch
1818

1919
from google.genai import types as genai_types
20+
from pydantic import BaseModel
2021

2122
from opentelemetry._logs import get_logger_provider
2223
from opentelemetry.instrumentation._semconv import (
@@ -35,6 +36,13 @@
3536
from ..common import otel_mocker
3637

3738

39+
class PydanticModel(BaseModel):
40+
"""Used to verify handling of pydantic models in the flattener."""
41+
42+
str_value: str = ""
43+
int_value: int = 0
44+
45+
3846
class TestCase(unittest.TestCase):
3947
def setUp(self):
4048
self._otel = otel_mocker.OTelMocker()
@@ -329,3 +337,19 @@ def somefunction(arg=None):
329337
span.attributes,
330338
)
331339
self.tearDown()
340+
341+
def test_handles_pydantic_class_not_instance(self):
342+
"""Test that passing a Pydantic class (not instance) doesn't cause model_dump() error."""
343+
344+
def somefunction(arg=None):
345+
return arg
346+
347+
wrapped_somefunction = self.wrap(somefunction)
348+
# Pass the class itself, not an instance
349+
wrapped_somefunction(PydanticModel)
350+
351+
# Should not raise "BaseModel.model_dump() missing 1 required positional argument: 'self'"
352+
span = self.otel.get_span_named("execute_tool somefunction")
353+
self.assertIsNotNone(span)
354+
# The class should be handled without error
355+
self.assertIn("code.function.parameters.arg.type", span.attributes)

0 commit comments

Comments
 (0)