diff --git a/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/test_sync_messages.py b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/test_sync_messages.py index f656f61eeff..18367d768d9 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/test_sync_messages.py +++ b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/test_sync_messages.py @@ -14,8 +14,11 @@ """Tests for sync Messages.create instrumentation.""" +from importlib.metadata import version as _package_version + import pytest from anthropic import Anthropic, APIConnectionError, NotFoundError +from packaging.version import Version from opentelemetry.instrumentation.anthropic import AnthropicInstrumentor from opentelemetry.semconv._incubating.attributes import ( @@ -28,6 +31,8 @@ server_attributes as ServerAttributes, ) +_anthropic_version = _package_version("anthropic") + def assert_span_attributes( span, @@ -114,6 +119,10 @@ def test_sync_messages_create_basic( ) +@pytest.mark.skipif( + Version(_anthropic_version) >= Version("1.0.0"), + reason="anthropic 1.0 removed temperature, top_p and top_k from messages.create()", +) @pytest.mark.vcr() def test_sync_messages_create_with_all_params( span_exporter, anthropic_client, instrument_no_content diff --git a/instrumentation-genai/opentelemetry-instrumentation-claude-agent-sdk/tests/requirements.oldest.txt b/instrumentation-genai/opentelemetry-instrumentation-claude-agent-sdk/tests/requirements.oldest.txt index adfadef283c..020d238b3cb 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-claude-agent-sdk/tests/requirements.oldest.txt +++ b/instrumentation-genai/opentelemetry-instrumentation-claude-agent-sdk/tests/requirements.oldest.txt @@ -16,7 +16,7 @@ # the oldest supported version of external dependencies. -e util/opentelemetry-util-genai -claude-agent-sdk==0.1.14 +claude-agent-sdk==0.1.19 # 0.1.14 has been removed from PyPI; 0.1.18 is yanked pytest==7.4.4 pytest-vcr==1.0.2 pytest-asyncio==0.21.0 diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/CHANGELOG.md b/instrumentation-genai/opentelemetry-instrumentation-google-genai/CHANGELOG.md index c0130ee1a1f..4cd9ca2af69 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/CHANGELOG.md +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - Relax version constraint of `google-genai` to allow v2 of that library to be used with the instrumentation library. +- Preserve tool parameter schemas supplied through `parameters_json_schema` in Google GenAI function declarations. ## Version 0.7b0 (2026-02-20) - Fix bug in how tokens are counted when using the streaming `generateContent` method. ([#4152](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4152)). diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/generate_content.py b/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/generate_content.py index 50feffec69c..7e6819d7eb0 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/generate_content.py +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/generate_content.py @@ -253,13 +253,14 @@ def _tool_to_tool_definition(tool: Tool) -> list[ToolDefinition]: definitions = [] if tool.function_declarations: for fd in tool.function_declarations: + parameters = getattr(fd, "parameters", None) + if parameters is None: + parameters = getattr(fd, "parameters_json_schema", None) definitions.append( FunctionToolDefinition( name=getattr(fd, "name", type(fd).__name__), description=getattr(fd, "description", None), - parameters=_clean_parameters( - getattr(fd, "parameters", None) - ), + parameters=_clean_parameters(parameters), ) ) diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/test_tool_definitions.py b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/test_tool_definitions.py new file mode 100644 index 00000000000..9fd45c5dc1c --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/test_tool_definitions.py @@ -0,0 +1,106 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +from unittest.mock import patch + +from google.genai import types + +from opentelemetry.instrumentation._semconv import ( + _OpenTelemetrySemanticConventionStability, + _OpenTelemetryStabilitySignalType, + _StabilityMode, +) + +from .base import TestCase + +_JSON_SCHEMA = { + "type": "object", + "properties": {"city": {"type": "string", "enum": ["Paris", "London"]}}, + "required": ["city"], + "additionalProperties": False, +} +_PARAMETERS = { + "type": "OBJECT", + "properties": {"city": {"type": "STRING"}}, + "required": ["city"], +} + + +def _expected(parameters): + return [ + { + "type": "function", + "name": "get_weather", + "description": "Get the weather for a city.", + "parameters": parameters, + } + ] + + +class ToolDefinitionParametersTestCase(TestCase): + def _get_tool_definitions(self, **parameter_fields): + # Patched per call: the base setUp replaces the stability mapping dict. + with ( + patch.dict( + "os.environ", + { + "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": "SPAN_ONLY", + "OTEL_SEMCONV_STABILITY_OPT_IN": "gen_ai_latest_experimental", + }, + ), + patch.dict( + _OpenTelemetrySemanticConventionStability._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING, + { + _OpenTelemetryStabilitySignalType.GEN_AI: _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL + }, + ), + ): + self.configure_valid_response(text="Sunny.") + declaration = types.FunctionDeclaration( + name="get_weather", + description="Get the weather for a city.", + **parameter_fields, + ) + self.client.models.generate_content( + model="gemini-2.5-flash", + contents="What is the weather in Paris?", + config=types.GenerateContentConfig( + tools=[types.Tool(function_declarations=[declaration])] + ), + ) + span = self.otel.get_span_named( + "generate_content gemini-2.5-flash" + ) + return json.loads(span.attributes["gen_ai.tool.definitions"]) + + def test_json_schema(self): + self.assertEqual( + self._get_tool_definitions(parameters_json_schema=_JSON_SCHEMA), + _expected(_JSON_SCHEMA), + ) + + def test_empty_json_schema(self): + self.assertEqual( + self._get_tool_definitions(parameters_json_schema={}), + _expected({}), + ) + + def test_parameters_wins(self): + self.assertEqual( + self._get_tool_definitions( + parameters=_PARAMETERS, parameters_json_schema=_JSON_SCHEMA + ), + _expected(_PARAMETERS), + ) diff --git a/instrumentation-genai/opentelemetry-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/callback_handler.py b/instrumentation-genai/opentelemetry-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/callback_handler.py index 8f642567ca1..4024f8826d9 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/callback_handler.py +++ b/instrumentation-genai/opentelemetry-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/callback_handler.py @@ -115,7 +115,7 @@ def on_chat_model_start( for sub_messages in messages: for message in sub_messages: # Cast to Any to avoid type checking issues with LangChain's complex content type - raw_content: Any = message.content # type: ignore[misc] + raw_content: Any = message.content role = message.type parts: list[Text] = [] diff --git a/instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai/patch.py b/instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai/patch.py index 481277d6964..0cc21ad3925 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai/patch.py +++ b/instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai/patch.py @@ -173,7 +173,7 @@ def handle_response( | None, ) -> None: attributes = ( - get_server_attributes(instance.api_endpoint) # type: ignore[reportUnknownMemberType] + get_server_attributes(instance.api_endpoint) | request_attributes | get_genai_response_attributes(response) ) @@ -257,7 +257,7 @@ def _with_default_instrumentation( kwargs: Any, ): params = _extract_params(*args, **kwargs) - api_endpoint: str = instance.api_endpoint # type: ignore[reportUnknownMemberType] + api_endpoint: str = instance.api_endpoint span_attributes = { **get_genai_request_attributes(False, params), **get_server_attributes(api_endpoint),