Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6f6c400
opentelementry-instrumentation-google-genai: add gen_ai.tool.definiti…
wikaaaaa Jan 23, 2026
befc106
Add _to_tool_definition
wikaaaaa Jan 26, 2026
e724d13
Remove unused import
wikaaaaa Jan 26, 2026
238580a
Add GEN_AI_TOOL_DEFINITIONS to tests.
wikaaaaa Jan 26, 2026
c786752
Remove uneccesary space.
wikaaaaa Jan 26, 2026
35f0c68
Merge branch 'main' into tooldefinitions
wikaaaaa Jan 26, 2026
daec2f6
Merge branch 'main' into tooldefinitions
wikaaaaa Jan 26, 2026
f0b7ecc
Merge branch 'main' into tooldefinitions
wikaaaaa Jan 27, 2026
4fa7323
address comments: add exclude_none to model_dump and tool type to err…
wikaaaaa Jan 28, 2026
aea8374
address comment: add if/else statement on tool types and add tests fo…
wikaaaaa Jan 29, 2026
74b343e
Merge branch 'main' into tooldefinitions
wikaaaaa Jan 29, 2026
270ae8b
dont serilize mcp client sessions in case of synchronous methods
wikaaaaa Jan 30, 2026
9acaa71
Refactor _to_tool_definition_common to be more clear
wikaaaaa Feb 2, 2026
60f66ae
remove uncessary 'function' key
wikaaaaa Feb 2, 2026
c19cd9c
fix failing tests: make mcp import conditional
wikaaaaa Feb 3, 2026
364aed2
Update changelog
wikaaaaa Feb 3, 2026
bafdbe1
Merge branch 'main' into tooldefinitions
wikaaaaa Feb 3, 2026
d679473
Update uv.lock
wikaaaaa Feb 3, 2026
f14cfb9
address comment: remove undecessary typing.Type
wikaaaaa Feb 4, 2026
1b07dcf
address comment: make mcp import conditional
wikaaaaa Feb 4, 2026
9371484
Merge branch 'main' into tooldefinitions
wikaaaaa Feb 4, 2026
7588cc1
Merge branch 'main' into tooldefinitions
wikaaaaa Feb 4, 2026
2d86bd5
Merge branch 'main' into tooldefinitions
wikaaaaa Feb 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
- Fix bug in how tokens are counted when using the streaming `generateContent` method. ([#4152](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4152)).

## Version 0.7b0 (2026-02-03)

- Add `gen_ai.tool.definitions` attribute to `gen_ai.client.inference.operation.details` log event ([#4142](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4142)).

## Version 0.6b0 (2026-01-27)

- Enable the addition of custom attributes to the `generate_content {model.name}` span via the Context API. ([#3961](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3961)).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import logging
import os
import time
import typing
from typing import (
Any,
AsyncIterator,
Expand All @@ -42,6 +43,9 @@
GenerateContentConfig,
GenerateContentConfigOrDict,
GenerateContentResponse,
Tool,
ToolListUnionDict,
ToolUnionDict,
)

from opentelemetry import context as context_api
Expand Down Expand Up @@ -82,8 +86,27 @@
from .otel_wrapper import OTelWrapper
from .tool_call_wrapper import wrapped as wrapped_tool

_is_mcp_imported = False
if typing.TYPE_CHECKING:
from mcp import ClientSession as McpClientSession
from mcp import Tool as McpTool

is_mcp_imported = True
else:
try:
from mcp import ClientSession as McpClientSession
from mcp import Tool as McpTool

_is_mcp_imported = True
except ImportError:
McpClientSession = None
McpTool = None

_logger = logging.getLogger(__name__)

GEN_AI_TOOL_DEFINITIONS = getattr(
gen_ai_attributes, "GEN_AI_TOOL_DEFINITIONS", "gen_ai.tool.definitions"
)

# Constant used to make the absence of content more understandable.
_CONTENT_ELIDED = "<elided>"
Expand Down Expand Up @@ -185,6 +208,68 @@ def _to_dict(value: object):
return json.loads(json.dumps(value))


def _tool_to_tool_definition(tool: ToolUnionDict) -> MessagePart:
if hasattr(tool, "model_dump"):
return tool.model_dump(exclude_none=True)

return str(tool)


def _callable_tool_to_tool_definition(tool: Any) -> MessagePart:
doc = getattr(tool, "__doc__", "") or ""
return {
"name": getattr(tool, "__name__", type(tool).__name__),
"description": doc.strip(),
}


def _mcp_tool_to_tool_definition(tool: McpTool) -> MessagePart:
if hasattr(tool, "model_dump"):
return tool.model_dump(exclude_none=True)

return {
"name": getattr(tool, "name", type(tool).__name__),
"description": getattr(tool, "description", "") or "",
"input_schema": getattr(tool, "input_schema", {}),
}


def _to_tool_definition_common(tool: ToolUnionDict) -> MessagePart:
if isinstance(tool, dict):
return tool

if isinstance(tool, Tool):
return _tool_to_tool_definition(tool)

if callable(tool):
return _callable_tool_to_tool_definition(tool)

if _is_mcp_imported and isinstance(tool, McpTool):
return _mcp_tool_to_tool_definition(tool)

try:
return {"raw_definition": json.loads(json.dumps(tool))}
except Exception: # pylint: disable=broad-exception-caught
return {
"error": f"failed to serialize tool definition, tool type={type(tool).__name__}"
}


def _to_tool_definition(tool: ToolUnionDict) -> MessagePart:
if _is_mcp_imported and isinstance(tool, McpClientSession):
return None

return _to_tool_definition_common(tool)


async def _to_tool_definition_async(tool: ToolUnionDict) -> MessagePart:
if _is_mcp_imported and isinstance(tool, McpClientSession):
result = await tool.list_tools()
return [t.model_dump(exclude_none=True) for t in result.tools]

return _to_tool_definition_common(tool)


def _create_request_attributes(
config: Optional[GenerateContentConfigOrDict],
allow_list: AllowList,
Expand Down Expand Up @@ -285,10 +370,22 @@ def _config_to_system_instruction(
return config.system_instruction


def _config_to_tools(
config: Union[GenerateContentConfigOrDict, None],
) -> Union[ToolListUnionDict, None]:
if not config:
return None

if isinstance(config, dict):
return GenerateContentConfig.model_validate(config).tools
return config.tools


def _create_completion_details_attributes(
input_messages: list[InputMessage],
output_messages: list[OutputMessage],
system_instructions: list[MessagePart],
tool_definitions: list[MessagePart],
as_str: bool = False,
) -> dict[str, AttributeValue]:
attributes: dict[str, AttributeValue] = {
Expand All @@ -306,6 +403,9 @@ def _create_completion_details_attributes(
dataclasses.asdict(sys_instr) for sys_instr in system_instructions
]

if tool_definitions:
attributes[GEN_AI_TOOL_DEFINITIONS] = tool_definitions

return attributes


Expand All @@ -324,6 +424,7 @@ def __init__(
model: str,
completion_hook: CompletionHook,
generate_content_config_key_allowlist: Optional[AllowList] = None,
is_async: bool = False,
):
self._start_time = time.time_ns()
self._otel_wrapper = otel_wrapper
Expand All @@ -345,6 +446,7 @@ def __init__(
self._generate_content_config_key_allowlist = (
generate_content_config_key_allowlist or AllowList()
)
self._is_async = is_async

def wrapped_config(
self, config: Optional[GenerateContentConfigOrDict]
Expand Down Expand Up @@ -461,6 +563,44 @@ def _maybe_update_error_type(self, response: GenerateContentResponse):
block_reason = response.prompt_feedback.block_reason.name.upper()
self._error_type = f"BLOCKED_{block_reason}"

def _maybe_get_tool_definitions(self, config):
if (
self.sem_conv_opt_in_mode
!= _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL
):
return None

tool_definitions = []
if tools := _config_to_tools(config):
for tool in tools:
definition = _to_tool_definition(tool)
if definition is None:
continue
if isinstance(definition, list):
tool_definitions.extend(definition)
else:
tool_definitions.append(definition)
return tool_definitions

async def _maybe_get_tool_definitions_async(self, config):
if (
self.sem_conv_opt_in_mode
!= _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL
):
return None

tool_definitions = []
if tools := _config_to_tools(config):
for tool in tools:
definition = await _to_tool_definition_async(tool)
if definition is None:
continue
if isinstance(definition, list):
tool_definitions.extend(definition)
else:
tool_definitions.append(definition)
return tool_definitions

def _maybe_log_completion_details(
self,
extra_attributes: dict[str, AttributeValue],
Expand All @@ -469,6 +609,7 @@ def _maybe_log_completion_details(
request: Union[ContentListUnion, ContentListUnionDict],
candidates: list[Candidate],
config: Optional[GenerateContentConfigOrDict] = None,
tool_definitions: list[MessagePart] = None,
):
if (
self.sem_conv_opt_in_mode
Expand Down Expand Up @@ -503,6 +644,7 @@ def _maybe_log_completion_details(
input_messages,
output_messages,
system_instructions,
tool_definitions,
)
if self._content_recording_enabled in [
ContentCapturingMode.EVENT_ONLY,
Expand Down Expand Up @@ -737,6 +879,7 @@ def instrumented_generate_content(
model,
completion_hook,
generate_content_config_key_allowlist=generate_content_config_key_allowlist,
is_async=False,
)
request_attributes = _create_request_attributes(
config,
Expand Down Expand Up @@ -774,13 +917,17 @@ def instrumented_generate_content(
finally:
final_attributes = helper.create_final_attributes()
span.set_attributes(final_attributes)
maybe_tool_definitions = helper._maybe_get_tool_definitions(
config
)
helper._maybe_log_completion_details(
extra_attributes,
request_attributes,
final_attributes,
contents,
candidates,
config,
maybe_tool_definitions,
)
helper._record_token_usage_metric()
helper._record_duration_metric()
Expand Down Expand Up @@ -812,6 +959,7 @@ def instrumented_generate_content_stream(
model,
completion_hook,
generate_content_config_key_allowlist=generate_content_config_key_allowlist,
is_async=False,
)
request_attributes = _create_request_attributes(
config,
Expand Down Expand Up @@ -849,13 +997,17 @@ def instrumented_generate_content_stream(
finally:
final_attributes = helper.create_final_attributes()
span.set_attributes(final_attributes)
maybe_tool_definitions = helper._maybe_get_tool_definitions(
config
)
helper._maybe_log_completion_details(
extra_attributes,
request_attributes,
final_attributes,
contents,
candidates,
config,
maybe_tool_definitions,
)
helper._record_token_usage_metric()
helper._record_duration_metric()
Expand Down Expand Up @@ -886,6 +1038,7 @@ async def instrumented_generate_content(
model,
completion_hook,
generate_content_config_key_allowlist=generate_content_config_key_allowlist,
is_async=True,
)
request_attributes = _create_request_attributes(
config,
Expand Down Expand Up @@ -923,13 +1076,17 @@ async def instrumented_generate_content(
finally:
final_attributes = helper.create_final_attributes()
span.set_attributes(final_attributes)
maybe_tool_definitions = (
await helper._maybe_get_tool_definitions_async(config)
)
helper._maybe_log_completion_details(
extra_attributes,
request_attributes,
final_attributes,
contents,
candidates,
config,
maybe_tool_definitions,
)
helper._record_token_usage_metric()
helper._record_duration_metric()
Expand Down Expand Up @@ -961,6 +1118,7 @@ async def instrumented_generate_content_stream(
model,
completion_hook,
generate_content_config_key_allowlist=generate_content_config_key_allowlist,
is_async=True,
)
request_attributes = _create_request_attributes(
config,
Expand Down Expand Up @@ -991,13 +1149,17 @@ async def instrumented_generate_content_stream(
helper._record_token_usage_metric()
final_attributes = helper.create_final_attributes()
span.set_attributes(final_attributes)
maybe_tool_definitions = (
await helper._maybe_get_tool_definitions_async(config)
)
helper._maybe_log_completion_details(
extra_attributes,
request_attributes,
final_attributes,
contents,
[],
config,
maybe_tool_definitions,
)
helper._record_duration_metric()
with trace.use_span(span, end_on_exit=True):
Expand Down Expand Up @@ -1025,13 +1187,19 @@ async def _response_async_generator_wrapper():
finally:
final_attributes = helper.create_final_attributes()
span.set_attributes(final_attributes)
maybe_tool_definitions = (
await helper._maybe_get_tool_definitions_async(
config
)
)
helper._maybe_log_completion_details(
extra_attributes,
request_attributes,
final_attributes,
contents,
candidates,
config,
maybe_tool_definitions,
)
helper._record_token_usage_metric()
helper._record_duration_metric()
Expand Down
Loading