Skip to content

Commit 971e8ec

Browse files
committed
fix(google-genai): handle functools.wraps outer wrapper in _parse_args
1 parent a151b38 commit 971e8ec

2 files changed

Lines changed: 54 additions & 11 deletions

File tree

python/instrumentation/openinference-instrumentation-google-genai/src/openinference/instrumentation/google_genai/_wrappers.py

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,25 @@ def _start_as_current_span(
127127

128128
def _parse_args(
129129
signature: Signature,
130+
instance: Any,
130131
*args: tuple[Any],
131132
**kwargs: Mapping[str, Any],
132133
) -> dict[str, Any]:
133-
bound_signature = signature.bind(*args, **kwargs)
134+
# When another library wraps a method using `functools.wraps` along with `wrapt`,
135+
# Python inspect.signature follows `__wrapped__` to the original unbound method
136+
# and reports ``self`` as a required positional parameter — but wrapt has already
137+
# consumed the instance and hands us only the user-supplied args. Detect this by trying
138+
# to bind as-is and, on failure, retry with `instance` prepended.
139+
# See issue #2995.
140+
params = signature.parameters
141+
needs_instance = params and next(iter(params.values())).name in ("self", "cls")
142+
if needs_instance:
143+
bound_signature = signature.bind(instance, *args, **kwargs)
144+
first_param = next(iter(signature.parameters.values()), None)
145+
if first_param and first_param.name in ("self", "cls"):
146+
bound_signature.arguments.pop(first_param.name, None)
147+
else:
148+
bound_signature = signature.bind(*args, **kwargs)
134149
bound_signature.apply_defaults()
135150
bound_arguments = bound_signature.arguments # Defaults empty to NOT_GIVEN
136151
request_data: dict[str, Any] = {}
@@ -169,7 +184,7 @@ def __call__(
169184
if context_api.get_value(context_api._SUPPRESS_INSTRUMENTATION_KEY):
170185
return wrapped(*args, **kwargs)
171186

172-
request_parameters = _parse_args(signature(wrapped), *args, **kwargs)
187+
request_parameters = _parse_args(signature(wrapped), instance, *args, **kwargs)
173188
span_name = "EmbedContent"
174189
with self._start_as_current_span(
175190
span_name=span_name,
@@ -227,7 +242,7 @@ async def __call__(
227242
if context_api.get_value(context_api._SUPPRESS_INSTRUMENTATION_KEY):
228243
return await wrapped(*args, **kwargs)
229244

230-
request_parameters = _parse_args(signature(wrapped), *args, **kwargs)
245+
request_parameters = _parse_args(signature(wrapped), instance, *args, **kwargs)
231246
span_name = "AsyncEmbedContent"
232247
with self._start_as_current_span(
233248
span_name=span_name,
@@ -285,7 +300,7 @@ def __call__(
285300
if context_api.get_value(context_api._SUPPRESS_INSTRUMENTATION_KEY):
286301
return wrapped(*args, **kwargs)
287302

288-
request_parameters = _parse_args(signature(wrapped), *args, **kwargs)
303+
request_parameters = _parse_args(signature(wrapped), instance, *args, **kwargs)
289304
span_name = "GenerateContent"
290305
with self._start_as_current_span(
291306
span_name=span_name,
@@ -335,7 +350,7 @@ def __call__(
335350
) -> Any:
336351
if context_api.get_value(context_api._SUPPRESS_INSTRUMENTATION_KEY):
337352
return wrapped(*args, **kwargs)
338-
request_parameters = _parse_args(signature(wrapped), *args, **kwargs)
353+
request_parameters = _parse_args(signature(wrapped), instance, *args, **kwargs)
339354
span_name = "InteractionsResource.create"
340355
with self._start_as_current_span(
341356
span_name=span_name,
@@ -386,7 +401,7 @@ def __call__(
386401
if context_api.get_value(context_api._SUPPRESS_INSTRUMENTATION_KEY):
387402
return wrapped(*args, **kwargs)
388403

389-
request_parameters = _parse_args(signature(wrapped), *args, **kwargs)
404+
request_parameters = _parse_args(signature(wrapped), instance, *args, **kwargs)
390405
span_name = "GenerateContentStream"
391406
with self._start_as_current_span(
392407
span_name=span_name,
@@ -443,7 +458,7 @@ async def __call__(
443458
if context_api.get_value(context_api._SUPPRESS_INSTRUMENTATION_KEY):
444459
return await wrapped(*args, **kwargs)
445460

446-
request_parameters = _parse_args(signature(wrapped), *args, **kwargs)
461+
request_parameters = _parse_args(signature(wrapped), instance, *args, **kwargs)
447462
span_name = "AsyncGenerateContent"
448463
with self._start_as_current_span(
449464
span_name=span_name,
@@ -500,7 +515,7 @@ async def __call__(
500515
if context_api.get_value(context_api._SUPPRESS_INSTRUMENTATION_KEY):
501516
return await wrapped(*args, **kwargs)
502517

503-
request_parameters = _parse_args(signature(wrapped), *args, **kwargs)
518+
request_parameters = _parse_args(signature(wrapped), instance, *args, **kwargs)
504519
span_name = "AsyncGenerateContentStream"
505520
with self._start_as_current_span(
506521
span_name=span_name,
@@ -549,7 +564,7 @@ async def __call__(
549564
) -> Any:
550565
if context_api.get_value(context_api._SUPPRESS_INSTRUMENTATION_KEY):
551566
return await wrapped(*args, **kwargs)
552-
request_parameters = _parse_args(signature(wrapped), *args, **kwargs)
567+
request_parameters = _parse_args(signature(wrapped), instance, *args, **kwargs)
553568
span_name = "AsyncInteractionsResource.create"
554569
with self._start_as_current_span(
555570
span_name=span_name,
@@ -593,7 +608,7 @@ def __call__(
593608
) -> Any:
594609
if context_api.get_value(context_api._SUPPRESS_INSTRUMENTATION_KEY):
595610
return wrapped(*args, **kwargs)
596-
request_parameters = _parse_args(signature(wrapped), *args, **kwargs)
611+
request_parameters = _parse_args(signature(wrapped), instance, *args, **kwargs)
597612
span_name = "Caches.create"
598613
status = trace_api.Status(status_code=trace_api.StatusCode.OK)
599614
with self._start_as_current_span(
@@ -632,7 +647,7 @@ async def __call__(
632647
) -> Any:
633648
if context_api.get_value(context_api._SUPPRESS_INSTRUMENTATION_KEY):
634649
return await wrapped(*args, **kwargs)
635-
request_parameters = _parse_args(signature(wrapped), *args, **kwargs)
650+
request_parameters = _parse_args(signature(wrapped), instance, *args, **kwargs)
636651
span_name = "AsyncCaches.create"
637652
status = trace_api.Status(status_code=trace_api.StatusCode.OK)
638653
with self._start_as_current_span(

python/instrumentation/openinference-instrumentation-google-genai/tests/test_instrumentation.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2135,3 +2135,31 @@ def test_validate_token_counts_stream(
21352135
assert attributes.get(key) == expected_value, (
21362136
f"Attribute {key} does not match expected value: got {attributes.get(key)}"
21372137
)
2138+
2139+
2140+
def test_parse_args_handle_functools_wraps_outer_wrapper() -> None:
2141+
import functools
2142+
from inspect import signature
2143+
2144+
from openinference.instrumentation.google_genai._wrappers import _parse_args
2145+
2146+
class _Fake:
2147+
def generate_content(self, *, model: str, contents: str) -> None:
2148+
pass
2149+
2150+
original = _Fake.generate_content
2151+
2152+
@functools.wraps(original)
2153+
def outer(self: Any, *args: Any, **kwargs: Any) -> None:
2154+
return original(self, *args, **kwargs)
2155+
2156+
instance = _Fake()
2157+
request_parameters = _parse_args(
2158+
signature(outer),
2159+
instance,
2160+
model="gemini-2.0-flash",
2161+
contents="hello",
2162+
)
2163+
assert "self" not in request_parameters
2164+
assert request_parameters["model"] == "gemini-2.0-flash"
2165+
assert request_parameters["contents"] == "hello"

0 commit comments

Comments
 (0)