diff --git a/instrumentation/opentelemetry-instrumentation-google-genai/.changelog/701.fixed b/instrumentation/opentelemetry-instrumentation-google-genai/.changelog/701.fixed new file mode 100644 index 000000000..88d7a735a --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-google-genai/.changelog/701.fixed @@ -0,0 +1 @@ +Capture provider-executed tool calls and results as server tool message parts. diff --git a/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/interactions.py b/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/interactions.py index b5459eaf5..8d84520e0 100644 --- a/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/interactions.py +++ b/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/interactions.py @@ -20,6 +20,7 @@ from google.genai._interactions.types.interaction_sse_event import ( InteractionSSEEvent, ) + from google.genai._interactions.types.step import Step _HAS_INTERACTIONS = True except ImportError: @@ -37,6 +38,7 @@ from google.genai._gaos.types.interactions import ( Interaction, InteractionSSEEvent, + Step, Usage, ) from google.genai._gaos.types.interactions import ( @@ -69,6 +71,9 @@ class Input: class InteractionSSEEvent: pass + class Step: + pass + class Stream: pass @@ -98,8 +103,11 @@ class Stream: GenericPart, GenericToolDefinition, InputMessage, + MessagePart, OutputMessage, Role, + ServerToolCallPart, + ServerToolCallResponsePart, TextPart, ToolCallRequestPart, ToolCallResponsePart, @@ -201,6 +209,103 @@ def _get_field(obj: Any, name: str) -> Any: return getattr(obj, name, None) +_SERVER_TOOL_CALL_NAMES = { + "code_execution_call": "code_execution", + "file_search_call": "file_search", + "google_maps_call": "google_maps", + "google_search_call": "google_search", + "mcp_server_tool_call": "mcp", + "processing_call": "processing", + "retrieval_call": "retrieval", + "url_context_call": "url_context", +} + +_SERVER_TOOL_RESPONSE_NAMES = { + "code_execution_result": "code_execution", + "file_search_result": "file_search", + "google_maps_result": "google_maps", + "google_search_result": "google_search", + "mcp_server_tool_result": "mcp", + "processing_result": "processing", + "retrieval_result": "retrieval", + "url_context_result": "url_context", +} + +_TOOL_STEP_TYPES = { + "function_call", + "function_result", + *_SERVER_TOOL_CALL_NAMES, + *_SERVER_TOOL_RESPONSE_NAMES, +} + + +def _to_server_tool_part( + item: Step | dict[str, object], +) -> MessagePart | None: + item_type = item.get("type") if isinstance(item, dict) else item.type + tool_name = _SERVER_TOOL_CALL_NAMES.get(item_type) + response_name = _SERVER_TOOL_RESPONSE_NAMES.get(item_type) + if tool_name is None and response_name is None: + return None + + if isinstance(item, dict): + payload = dict(item) + else: + payload = item.model_dump(exclude_none=True, mode="json") + + item_id = payload.pop("id", None) + call_id = payload.pop("call_id", None) + payload.pop("type", None) + name = payload.pop("name", None) + canonical_name = tool_name or response_name + if canonical_name is None: + return None + payload["type"] = canonical_name + + if response_name is not None: + return ServerToolCallResponsePart( + id=call_id if isinstance(call_id, str) else None, + server_tool_call_response=payload, + ) + return ServerToolCallPart( + id=item_id if isinstance(item_id, str) else None, + name=name if isinstance(name, str) else canonical_name, + server_tool_call=payload, + ) + + +def _interaction_item_to_part( + item: Step | dict[str, object], +) -> MessagePart | None: + if isinstance(item, dict): + item_type = item.get("type") + item_id = item.get("id") + name = item.get("name") + arguments = item.get("arguments") + call_id = item.get("call_id") + result = item.get("result") + else: + item_type = item.type + item_id = item.id if item_type == "function_call" else None + name = item.name if item_type == "function_call" else None + arguments = item.arguments if item_type == "function_call" else None + call_id = item.call_id if item_type == "function_result" else None + result = item.result if item_type == "function_result" else None + + if item_type == "function_call": + return ToolCallRequestPart( + id=item_id if isinstance(item_id, str) else None, + name=name if isinstance(name, str) else "", + arguments=arguments, + ) + if item_type == "function_result": + return ToolCallResponsePart( + id=call_id if isinstance(call_id, str) else None, + response=result, + ) + return _to_server_tool_part(item) + + # Logic for parsing Input is tricky: # https://github.com/open-telemetry/donation-openinference/blob/6cdd644d79fccf50aedcb614187f924ddfcafb7b/python/instrumentation/openinference-instrumentation-google-genai/src/openinference/instrumentation/google_genai/interactions_attributes.py#L103 # It doesn't make sense for this to be a List[InputMessage] (per semconv), @@ -222,24 +327,21 @@ def _interactions_input_to_messages( if not isinstance(input_data, Sequence): input_data = [input_data] - parts = [] + parts: list[MessagePart] = [] for item in input_data: - item_type = _get_field(item, "type") - if item_type == "function_call": - call_id = _get_field(item, "id") - name = _get_field(item, "name") - arguments = _get_field(item, "arguments") - part = ToolCallRequestPart( - id=call_id, name=name or "", arguments=arguments - ) - parts.append(part) - elif item_type == "function_result": - call_id = _get_field(item, "call_id") - result = _get_field(item, "result") - part = ToolCallResponsePart(id=call_id, response=result) - parts.append(part) - elif isinstance(item, str): + if isinstance(item, str): parts.append(TextPart(content=item)) + continue + + item_type = _get_field(item, "type") + message_part = None + if isinstance(item, dict): + message_part = _interaction_item_to_part(item) + elif item_type in _TOOL_STEP_TYPES: + message_part = _interaction_item_to_part(cast(Step, item)) + + if message_part is not None: + parts.append(message_part) elif item_type == "text": part = TextPart(content=_get_field(item, "text") or "") parts.append(part) @@ -283,11 +385,26 @@ def _get_interaction_output_text(interaction: Interaction) -> str: def _interactions_response_to_messages( interaction: Interaction, ) -> list[OutputMessage]: - output_text = _get_interaction_output_text(interaction) + parts: list[MessagePart] = [] + for step in interaction.steps or []: + if part := _interaction_item_to_part(step): + parts.append(part) + continue + if step.type != "model_output": + continue + for item in step.content or []: + if item.type == "text" and isinstance(item.text, str): + text = item.text + parts.append(TextPart(content=text)) + + if not any(isinstance(part, TextPart) for part in parts): + parts.append( + TextPart(content=_get_interaction_output_text(interaction)) + ) return [ OutputMessage( role=Role.ASSISTANT.value, - parts=[TextPart(content=output_text)], + parts=parts, finish_reason="stop", ) ] diff --git a/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/message.py b/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/message.py index aabdf2336..febfef29b 100644 --- a/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/message.py +++ b/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/message.py @@ -15,6 +15,8 @@ MessagePart, OutputMessage, Role, + ServerToolCallPart, + ServerToolCallResponsePart, SystemInstructionPart, TextPart, ToolCallRequestPart, @@ -122,6 +124,52 @@ def tool_call_id(name: str | None) -> str: response=response.response, ) + if call := getattr(part, "tool_call", None): + name = call.tool_type.value.lower() if call.tool_type else "unknown" + return ServerToolCallPart( + id=call.id, + name=name, + server_tool_call={ + "type": name, + "arguments": call.args, + }, + ) + + if response := getattr(part, "tool_response", None): + name = ( + response.tool_type.value.lower() + if response.tool_type + else "unknown" + ) + return ServerToolCallResponsePart( + id=response.id, + server_tool_call_response={ + "type": name, + "response": response.response, + }, + ) + + if code := part.executable_code: + return ServerToolCallPart( + id=getattr(code, "id", None), + name="code_execution", + server_tool_call={ + "type": "code_execution", + "code": code.code, + "language": code.language.value if code.language else None, + }, + ) + + if result := part.code_execution_result: + return ServerToolCallResponsePart( + id=getattr(result, "id", None), + server_tool_call_response={ + "type": "code_execution", + "outcome": result.outcome.value if result.outcome else None, + "output": result.output, + }, + ) + _logger.info("Unknown part dropped from telemetry %s", part) return None diff --git a/instrumentation/opentelemetry-instrumentation-google-genai/tests/cassettes/generate_content_server_tool_calling_conformance.yaml b/instrumentation/opentelemetry-instrumentation-google-genai/tests/cassettes/generate_content_server_tool_calling_conformance.yaml new file mode 100644 index 000000000..9e02497d1 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-google-genai/tests/cassettes/generate_content_server_tool_calling_conformance.yaml @@ -0,0 +1,58 @@ +# TODO: this is generated by AI, re-record +interactions: +- request: + body: '{"contents":[{"parts":[{"text":"Run Python to print one."}],"role":"user"}],"tools":[{"codeExecution":{}}]}' + headers: + content-type: + - application/json + host: + - generativelanguage.googleapis.com + x-goog-api-key: + - test_google_genai_api_key + method: POST + uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent + response: + body: + string: |- + { + "candidates": [ + { + "content": { + "parts": [ + { + "executableCode": { + "id": "code-1", + "code": "print(1)", + "language": "PYTHON" + } + }, + { + "codeExecutionResult": { + "id": "code-1", + "outcome": "OUTCOME_OK", + "output": "1" + } + }, + {"text": "The result is 1."} + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0 + } + ], + "usageMetadata": { + "promptTokenCount": 8, + "candidatesTokenCount": 10, + "totalTokenCount": 18 + }, + "modelVersion": "gemini-2.5-flash", + "responseId": "response-code-1" + } + headers: + content-type: + - application/json; charset=UTF-8 + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation/opentelemetry-instrumentation-google-genai/tests/cassettes/interactions_server_tool_calling_conformance.yaml b/instrumentation/opentelemetry-instrumentation-google-genai/tests/cassettes/interactions_server_tool_calling_conformance.yaml new file mode 100644 index 000000000..e30711ed2 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-google-genai/tests/cassettes/interactions_server_tool_calling_conformance.yaml @@ -0,0 +1,55 @@ +# TODO: this is generated by AI, re-record +interactions: +- request: + body: |- + {"model":"gemini-2.5-flash","input":"Search for OpenTelemetry.","tools":[{"type":"google_search"}]} + headers: + content-type: + - application/json + host: + - generativelanguage.googleapis.com + method: POST + uri: https://generativelanguage.googleapis.com/v1beta/interactions + response: + body: + string: |- + { + "id": "interaction-search-1", + "status": "completed", + "usage": { + "total_tokens": 30, + "total_input_tokens": 10, + "total_output_tokens": 20, + "total_cached_tokens": 0, + "total_thought_tokens": 0 + }, + "steps": [ + { + "type": "google_search_call", + "id": "search-1", + "arguments": {"queries": ["OpenTelemetry"]}, + "search_type": "web_search" + }, + { + "type": "google_search_result", + "call_id": "search-1", + "result": [], + "is_error": false + }, + { + "type": "model_output", + "content": [ + {"type": "text", "text": "OpenTelemetry is an observability framework."} + ] + } + ], + "object": "interaction", + "model": "gemini-2.5-flash" + } + headers: + content-type: + - application/json + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation/opentelemetry-instrumentation-google-genai/tests/conformance/server_tool_calling.py b/instrumentation/opentelemetry-instrumentation-google-genai/tests/conformance/server_tool_calling.py new file mode 100644 index 000000000..45513bc28 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-google-genai/tests/conformance/server_tool_calling.py @@ -0,0 +1,160 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +"""Conformance scenarios: Google GenAI server-side tool calls.""" + +from __future__ import annotations + +import json +from typing import Any + +from google.genai import Client + +from opentelemetry.instrumentation.google_genai import ( + GoogleGenAiSdkInstrumentor, +) +from opentelemetry.sdk._logs import LoggerProvider +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.test.weaver_live_check import LiveCheckReport +from opentelemetry.test_util_genai.conformance import ( + ExpectedViolation, + Scenario, +) +from opentelemetry.test_util_genai.instrumentor import instrument + + +def _output_parts(report: LiveCheckReport) -> list[dict[str, Any]]: + output_messages = [ + json.loads(attribute["value"]) + for entry in report["samples"] + if "span" in entry + for attribute in entry["span"]["attributes"] + if attribute["name"] == "gen_ai.output.messages" + ] + assert len(output_messages) == 1 + return output_messages[0][0]["parts"] + + +class GenerateContentServerToolCallingScenario(Scenario): + expected_spans = {"generate_content": 1} + expected_metrics = ( + "gen_ai.client.operation.duration", + "gen_ai.client.token.usage", + ) + + def validate(self, report: LiveCheckReport) -> None: + super().validate(report) + assert _output_parts(report)[:2] == [ + { + "name": "code_execution", + "server_tool_call": { + "code": "print(1)", + "language": "PYTHON", + "type": "code_execution", + }, + "id": "code-1", + "type": "server_tool_call", + }, + { + "server_tool_call_response": { + "outcome": "OUTCOME_OK", + "output": "1", + "type": "code_execution", + }, + "id": "code-1", + "type": "server_tool_call_response", + }, + ] + + def run( + self, + *, + tracer_provider: TracerProvider, + meter_provider: MeterProvider, + logger_provider: LoggerProvider, + vcr: Any, + ) -> None: + with instrument( + GoogleGenAiSdkInstrumentor(), + tracer_provider=tracer_provider, + logger_provider=logger_provider, + meter_provider=meter_provider, + content_capture="SPAN_ONLY", + ): + with vcr.use_cassette( + "generate_content_server_tool_calling_conformance.yaml" + ): + client = Client( + api_key="test_google_genai_api_key", vertexai=False + ) + client.models.generate_content( + model="gemini-2.5-flash", + contents="Run Python to print one.", + config={"tools": [{"code_execution": {}}]}, + ) + + +class InteractionsServerToolCallingScenario(Scenario): + expected_spans = {"interactions.create": 1} + expected_metrics = ( + "gen_ai.client.operation.duration", + "gen_ai.client.token.usage", + ) + expected_violations = ( + ExpectedViolation( + advice_id="genai_operation_name_unknown", + message_substring="interactions.create", + ), + ) + + def validate(self, report: LiveCheckReport) -> None: + super().validate(report) + assert _output_parts(report)[:2] == [ + { + "name": "google_search", + "server_tool_call": { + "arguments": {"queries": ["OpenTelemetry"]}, + "search_type": "web_search", + "type": "google_search", + }, + "id": "search-1", + "type": "server_tool_call", + }, + { + "server_tool_call_response": { + "is_error": False, + "result": [], + "type": "google_search", + }, + "id": "search-1", + "type": "server_tool_call_response", + }, + ] + + def run( + self, + *, + tracer_provider: TracerProvider, + meter_provider: MeterProvider, + logger_provider: LoggerProvider, + vcr: Any, + ) -> None: + with instrument( + GoogleGenAiSdkInstrumentor(), + tracer_provider=tracer_provider, + logger_provider=logger_provider, + meter_provider=meter_provider, + content_capture="SPAN_ONLY", + ): + with vcr.use_cassette( + "interactions_server_tool_calling_conformance.yaml" + ): + client = Client( + api_key="test_google_genai_api_key", vertexai=False + ) + client.interactions.create( + model="gemini-2.5-flash", + input="Search for OpenTelemetry.", + tools=[{"type": "google_search"}], + ) diff --git a/instrumentation/opentelemetry-instrumentation-google-genai/tests/generate_content/test_message.py b/instrumentation/opentelemetry-instrumentation-google-genai/tests/generate_content/test_message.py index 577335959..8aed71709 100644 --- a/instrumentation/opentelemetry-instrumentation-google-genai/tests/generate_content/test_message.py +++ b/instrumentation/opentelemetry-instrumentation-google-genai/tests/generate_content/test_message.py @@ -6,9 +6,15 @@ from google.genai import types as genai_types from opentelemetry.instrumentation.google_genai.message import ( + _to_part, to_system_instructions, ) -from opentelemetry.util.genai.types import GenericPart, TextPart +from opentelemetry.util.genai.types import ( + GenericPart, + ServerToolCallPart, + ServerToolCallResponsePart, + TextPart, +) class TestGoogleGenAiMessage(unittest.TestCase): @@ -46,3 +52,103 @@ def test_to_system_instructions_empty_or_unknown_part(self): content = genai_types.Content(parts=[genai_types.Part()]) instructions = to_system_instructions(content=content) self.assertEqual(instructions, []) + + def test_to_part_maps_code_execution(self): + call_id = ( + {"id": "code-1"} + if "id" in genai_types.ExecutableCode.model_fields + else {} + ) + response_id = ( + {"id": "code-1"} + if "id" in genai_types.CodeExecutionResult.model_fields + else {} + ) + call = _to_part( + genai_types.Part( + executable_code=genai_types.ExecutableCode( + code="print(1)", + language=genai_types.Language.PYTHON, + **call_id, + ) + ), + 0, + ) + response = _to_part( + genai_types.Part( + code_execution_result=genai_types.CodeExecutionResult( + outcome=genai_types.Outcome.OUTCOME_OK, + output="1", + **response_id, + ) + ), + 1, + ) + + self.assertIsInstance(call, ServerToolCallPart) + self.assertEqual(call.id, call_id.get("id")) + self.assertEqual(call.name, "code_execution") + self.assertEqual( + call.server_tool_call, + { + "type": "code_execution", + "code": "print(1)", + "language": "PYTHON", + }, + ) + self.assertIsInstance(response, ServerToolCallResponsePart) + self.assertEqual(response.id, response_id.get("id")) + self.assertEqual( + response.server_tool_call_response, + { + "type": "code_execution", + "outcome": "OUTCOME_OK", + "output": "1", + }, + ) + + @unittest.skipUnless( + "tool_call" in genai_types.Part.model_fields, + "server tool parts are unavailable in this SDK", + ) + def test_to_part_maps_server_tool_call_and_response(self): + call = _to_part( + genai_types.Part( + tool_call=genai_types.ToolCall( + id="tool-1", + tool_type=genai_types.ToolType.GOOGLE_SEARCH_WEB, + args={"query": "OpenTelemetry"}, + ) + ), + 0, + ) + response = _to_part( + genai_types.Part( + tool_response=genai_types.ToolResponse( + id="tool-1", + tool_type=genai_types.ToolType.GOOGLE_SEARCH_WEB, + response={"results": []}, + ) + ), + 1, + ) + + self.assertIsInstance(call, ServerToolCallPart) + self.assertEqual(call.id, "tool-1") + self.assertEqual(call.name, "google_search_web") + self.assertEqual( + call.server_tool_call, + { + "type": "google_search_web", + "arguments": {"query": "OpenTelemetry"}, + }, + ) + self.assertIsInstance(response, ServerToolCallResponsePart) + self.assertEqual(response.id, "tool-1") + self.assertEqual( + response.server_tool_call_response, + { + "type": "google_search_web", + "response": {"results": []}, + }, + ) diff --git a/instrumentation/opentelemetry-instrumentation-google-genai/tests/interactions/test_parser.py b/instrumentation/opentelemetry-instrumentation-google-genai/tests/interactions/test_parser.py index cc4a2c539..7ef2b6dfe 100644 --- a/instrumentation/opentelemetry-instrumentation-google-genai/tests/interactions/test_parser.py +++ b/instrumentation/opentelemetry-instrumentation-google-genai/tests/interactions/test_parser.py @@ -8,11 +8,14 @@ from opentelemetry.instrumentation.google_genai.interactions import ( _HAS_INTERACTIONS, + Interaction, _interactions_input_to_messages, _interactions_response_to_messages, ) from opentelemetry.util.genai.types import ( GenericPart, + ServerToolCallPart, + ServerToolCallResponsePart, TextPart, ToolCallRequestPart, ToolCallResponsePart, @@ -97,6 +100,39 @@ def test_input_to_messages_tool_result_step(self) -> None: self.assertEqual(messages[0].parts[0].id, "call-123") self.assertEqual(messages[0].parts[0].response, {"val": 10}) + def test_input_to_messages_server_tool_steps(self) -> None: + steps = [ + { + "type": "google_search_call", + "id": "search-1", + "arguments": {"query": "OpenTelemetry"}, + }, + { + "type": "google_search_result", + "call_id": "search-1", + "result": [], + }, + ] + + parts = _interactions_input_to_messages(steps)[0].parts + + self.assertIsInstance(parts[0], ServerToolCallPart) + self.assertEqual(parts[0].id, "search-1") + self.assertEqual(parts[0].name, "google_search") + self.assertEqual( + parts[0].server_tool_call, + { + "arguments": {"query": "OpenTelemetry"}, + "type": "google_search", + }, + ) + self.assertIsInstance(parts[1], ServerToolCallResponsePart) + self.assertEqual(parts[1].id, "search-1") + self.assertEqual( + parts[1].server_tool_call_response, + {"result": [], "type": "google_search"}, + ) + def test_input_to_messages_generic_fallback(self) -> None: steps = [{"type": "some_unsupported_type"}] messages = _interactions_input_to_messages(steps) @@ -127,3 +163,61 @@ def test_response_to_messages(self) -> None: self.assertEqual(len(messages[0].parts), 1) self.assertIsInstance(messages[0].parts[0], TextPart) self.assertEqual(messages[0].parts[0].content, "Model response text") + + def test_response_to_messages_includes_tool_steps(self) -> None: + interaction = Interaction.model_validate( + { + "id": "interaction-1", + "created": "2026-06-24T18:51:26Z", + "updated": "2026-06-24T18:51:26Z", + "status": "completed", + "output_text": "Search complete", + "steps": [ + { + "type": "mcp_server_tool_call", + "id": "mcp-1", + "name": "search", + "server_name": "docs", + "arguments": {"query": "OpenTelemetry"}, + }, + { + "type": "mcp_server_tool_result", + "call_id": "mcp-1", + "name": "search", + "server_name": "docs", + "result": {"items": []}, + }, + { + "type": "model_output", + "content": [ + {"type": "text", "text": "Search complete"} + ], + }, + ], + } + ) + + parts = _interactions_response_to_messages(interaction)[0].parts + + self.assertIsInstance(parts[0], ServerToolCallPart) + self.assertEqual(parts[0].id, "mcp-1") + self.assertEqual(parts[0].name, "search") + self.assertEqual( + parts[0].server_tool_call, + { + "server_name": "docs", + "arguments": {"query": "OpenTelemetry"}, + "type": "mcp", + }, + ) + self.assertIsInstance(parts[1], ServerToolCallResponsePart) + self.assertEqual(parts[1].id, "mcp-1") + self.assertEqual( + parts[1].server_tool_call_response, + { + "server_name": "docs", + "result": {"items": []}, + "type": "mcp", + }, + ) + self.assertEqual(parts[2], TextPart(content="Search complete")) diff --git a/instrumentation/opentelemetry-instrumentation-google-genai/tests/test_conformance.py b/instrumentation/opentelemetry-instrumentation-google-genai/tests/test_conformance.py index f337627bf..909a66a10 100644 --- a/instrumentation/opentelemetry-instrumentation-google-genai/tests/test_conformance.py +++ b/instrumentation/opentelemetry-instrumentation-google-genai/tests/test_conformance.py @@ -26,6 +26,10 @@ from .conformance.embedding import EmbeddingScenario from .conformance.generate_content import GenerateContentScenario from .conformance.inference import InferenceScenario +from .conformance.server_tool_calling import ( + GenerateContentServerToolCallingScenario, + InteractionsServerToolCallingScenario, +) from .conformance.tool_calling import ToolCallingScenario @@ -36,6 +40,8 @@ EmbeddingScenario(), GenerateContentScenario(), ToolCallingScenario(), + GenerateContentServerToolCallingScenario(), + InteractionsServerToolCallingScenario(), ], ids=lambda s: type(s).__name__, )