diff --git a/python/packages/core/agent_framework/observability.py b/python/packages/core/agent_framework/observability.py index d324caa757..4806ea6823 100644 --- a/python/packages/core/agent_framework/observability.py +++ b/python/packages/core/agent_framework/observability.py @@ -101,6 +101,12 @@ INNER_RESPONSE_TELEMETRY_CAPTURED_FIELDS: Final[contextvars.ContextVar[set[str] INNER_RESPONSE_ID_CAPTURED_FIELD: Final[str] = "response_id" INNER_USAGE_CAPTURED_FIELD: Final[str] = "usage" +# Response header set by Azure OpenAI naming the model that actually served the +# request (which can differ from the deployment alias the caller sent). Chat +# clients may surface this on ``ChatResponse.additional_properties`` so the +# telemetry layer can promote it to ``gen_ai.response.model``. +AZURE_OPENAI_SERVED_MODEL_HEADER: Final[str] = "x-ms-served-model" + # Tracks accumulated token usage from all inner chat completion spans within an agent invoke. INNER_ACCUMULATED_USAGE: Final[contextvars.ContextVar[UsageDetails | None]] = contextvars.ContextVar( "inner_accumulated_usage", default=None @@ -2125,6 +2131,14 @@ def _get_response_attributes( attributes[OtelAttr.FINISH_REASONS] = json.dumps([finish_reason]) if model := getattr(response, "model", None): attributes[OtelAttr.RESPONSE_MODEL] = model + # If the underlying provider reports the actually served model via the + # ``x-ms-served-model`` response header (Azure OpenAI), prefer it over the + # model reported on the response body for the response model attribute. + additional_properties = getattr(response, "additional_properties", None) + if isinstance(additional_properties, Mapping): + candidate = cast("Mapping[str, Any]", additional_properties).get(AZURE_OPENAI_SERVED_MODEL_HEADER) + if isinstance(candidate, str) and candidate: + attributes[OtelAttr.RESPONSE_MODEL] = candidate if capture_usage and (usage := response.usage_details): input_tokens = usage.get("input_token_count") if input_tokens: diff --git a/python/packages/core/tests/core/test_observability.py b/python/packages/core/tests/core/test_observability.py index 71b59a351b..9a1ed11a29 100644 --- a/python/packages/core/tests/core/test_observability.py +++ b/python/packages/core/tests/core/test_observability.py @@ -1739,6 +1739,78 @@ def test_get_response_attributes_capture_response_id_false(): assert OtelAttr.RESPONSE_ID not in result +def test_get_response_attributes_served_model_overrides_response_model(): + """When the response carries the Azure ``x-ms-served-model`` header, it should override RESPONSE_MODEL.""" + from unittest.mock import Mock + + from agent_framework.observability import ( + AZURE_OPENAI_SERVED_MODEL_HEADER, + OtelAttr, + _get_response_attributes, + ) + + response = Mock() + response.response_id = None + response.finish_reason = None + response.raw_representation = None + response.usage_details = None + response.model = "gpt-4" + response.additional_properties = {AZURE_OPENAI_SERVED_MODEL_HEADER: "gpt-4o-2024-08-06"} + + attrs = {OtelAttr.REQUEST_MODEL: "my-deployment-alias"} + result = _get_response_attributes(attrs, response) + + # REQUEST_MODEL is left untouched; RESPONSE_MODEL is overridden by the served-model header. + assert result[OtelAttr.REQUEST_MODEL] == "my-deployment-alias" + assert result[OtelAttr.RESPONSE_MODEL] == "gpt-4o-2024-08-06" + + +def test_get_response_attributes_no_served_model_keeps_response_model(): + """Without the served-model header RESPONSE_MODEL should reflect the response's reported model.""" + from unittest.mock import Mock + + from agent_framework.observability import OtelAttr, _get_response_attributes + + response = Mock() + response.response_id = None + response.finish_reason = None + response.raw_representation = None + response.usage_details = None + response.model = "gpt-4" + response.additional_properties = {} + + attrs = {OtelAttr.REQUEST_MODEL: "my-deployment-alias"} + result = _get_response_attributes(attrs, response) + + assert result[OtelAttr.REQUEST_MODEL] == "my-deployment-alias" + assert result[OtelAttr.RESPONSE_MODEL] == "gpt-4" + + +def test_get_response_attributes_ignores_non_string_served_model(): + """A non-string / empty value in the served-model header should not override RESPONSE_MODEL.""" + from unittest.mock import Mock + + from agent_framework.observability import ( + AZURE_OPENAI_SERVED_MODEL_HEADER, + OtelAttr, + _get_response_attributes, + ) + + response = Mock() + response.response_id = None + response.finish_reason = None + response.raw_representation = None + response.usage_details = None + response.model = "gpt-4" + response.additional_properties = {AZURE_OPENAI_SERVED_MODEL_HEADER: ""} + + attrs = {OtelAttr.REQUEST_MODEL: "my-deployment-alias"} + result = _get_response_attributes(attrs, response) + + assert result[OtelAttr.REQUEST_MODEL] == "my-deployment-alias" + assert result[OtelAttr.RESPONSE_MODEL] == "gpt-4" + + # region Test _get_exporters_from_env @@ -2481,6 +2553,28 @@ def test_capture_response(span_exporter: InMemorySpanExporter): assert spans[0].attributes.get(OtelAttr.OUTPUT_TOKENS) == 50 +def test_capture_response_does_not_update_span_name_with_request_model(span_exporter: InMemorySpanExporter): + """_capture_response should not rename the span even when REQUEST_MODEL is set.""" + from agent_framework.observability import OtelAttr, _capture_response, get_tracer + + span_exporter.clear() + tracer = get_tracer() + + attrs = { + OtelAttr.OPERATION: "chat", + OtelAttr.REQUEST_MODEL: "my-deployment-alias", + OtelAttr.RESPONSE_MODEL: "gpt-4o-2024-08-06", + } + + with tracer.start_as_current_span("chat my-deployment-alias") as span: + _capture_response(span=span, attributes=attrs) + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + assert spans[0].name == "chat my-deployment-alias" + assert spans[0].attributes.get(OtelAttr.RESPONSE_MODEL) == "gpt-4o-2024-08-06" + + async def test_layer_ordering_span_sequence_with_function_calling(span_exporter: InMemorySpanExporter): """Test that with correct layer ordering, spans appear in the expected sequence. diff --git a/python/packages/openai/agent_framework_openai/_chat_client.py b/python/packages/openai/agent_framework_openai/_chat_client.py index 40d9063b12..c39574e474 100644 --- a/python/packages/openai/agent_framework_openai/_chat_client.py +++ b/python/packages/openai/agent_framework_openai/_chat_client.py @@ -62,8 +62,9 @@ from agent_framework.exceptions import ( ChatClientException, ChatClientInvalidRequestException, ) -from agent_framework.observability import ChatTelemetryLayer +from agent_framework.observability import AZURE_OPENAI_SERVED_MODEL_HEADER, ChatTelemetryLayer from openai import AsyncAzureOpenAI, AsyncOpenAI, BadRequestError +from openai.lib._parsing._responses import type_to_text_format_param as _type_to_text_format_param from openai.types.responses import FunctionShellTool from openai.types.responses.file_search_tool_param import FileSearchToolParam from openai.types.responses.function_tool_param import FunctionToolParam @@ -359,6 +360,11 @@ class RawOpenAIChatClient( # type: ignore[misc] STORES_BY_DEFAULT: ClassVar[bool] = True # type: ignore[reportIncompatibleVariableOverride, misc] SUPPORTS_RICH_FUNCTION_OUTPUT: ClassVar[bool] = True + # Azure OpenAI may include this header in responses for the actual model that served the request, + # instead of the deployment name sent in the request. Surface it on the ChatResponse when present + # for better observability. + SERVED_MODEL_HEADER: ClassVar[str] = AZURE_OPENAI_SERVED_MODEL_HEADER + FILE_SEARCH_MAX_RESULTS: int = 50 @overload @@ -614,17 +620,24 @@ class RawOpenAIChatClient( # type: ignore[misc] client = self.client validated_options = await self._validate_options(options) try: - stream_response = await client.responses.retrieve( + raw_stream_response = await client.responses.with_raw_response.retrieve( continuation_token["response_id"], stream=True, ) + served_model = self._extract_served_model(raw_stream_response.headers) + stream_response = raw_stream_response.parse() async for chunk in stream_response: - yield self._parse_chunk_from_openai( + update = self._parse_chunk_from_openai( chunk, options=validated_options, function_call_ids=function_call_ids, seen_reasoning_delta_item_ids=seen_reasoning_delta_item_ids, ) + if served_model is not None: + if update.additional_properties is None: + update.additional_properties = {} + update.additional_properties[self.SERVED_MODEL_HEADER] = served_model + yield update except Exception as ex: self._handle_request_error(ex) else: @@ -633,24 +646,35 @@ class RawOpenAIChatClient( # type: ignore[misc] run_options, validated_options, ) = await self._prepare_request(messages, options) + # Translate `text_format` (Pydantic model) into `text.format` JSON-schema + # config so we can always go through `responses.create(stream=True, ...)` — + # the only path that exposes raw HTTP headers via `with_raw_response`. + # The SDK's `responses.stream(...)` helper does the same translation + # internally but does not surface headers, so we replicate it here. + text_format = run_options.pop("text_format", None) + if text_format is not None: + text_cfg = dict(run_options.get("text") or {}) + if "format" in text_cfg: + raise ChatClientInvalidRequestException("Cannot mix and match text.format with text_format") + text_cfg["format"] = _type_to_text_format_param(text_format) + run_options["text"] = text_cfg try: - if "text_format" in run_options: - async with client.responses.stream(**run_options) as response: - async for chunk in response: - yield self._parse_chunk_from_openai( - chunk, - options=validated_options, - function_call_ids=function_call_ids, - seen_reasoning_delta_item_ids=seen_reasoning_delta_item_ids, - ) - else: - async for chunk in await client.responses.create(stream=True, **run_options): - yield self._parse_chunk_from_openai( - chunk, - options=validated_options, - function_call_ids=function_call_ids, - seen_reasoning_delta_item_ids=seen_reasoning_delta_item_ids, - ) + raw_create_response = await client.responses.with_raw_response.create( + stream=True, **run_options + ) + served_model = self._extract_served_model(raw_create_response.headers) + async for chunk in raw_create_response.parse(): + update = self._parse_chunk_from_openai( + chunk, + options=validated_options, + function_call_ids=function_call_ids, + seen_reasoning_delta_item_ids=seen_reasoning_delta_item_ids, + ) + if served_model is not None: + if update.additional_properties is None: + update.additional_properties = {} + update.additional_properties[self.SERVED_MODEL_HEADER] = served_model + yield update except Exception as ex: self._handle_request_error(ex) @@ -664,10 +688,12 @@ class RawOpenAIChatClient( # type: ignore[misc] client = self.client validated_options = await self._validate_options(options) try: - response = await client.responses.retrieve(continuation_token["response_id"]) + raw_response = await client.responses.with_raw_response.retrieve(continuation_token["response_id"]) + response = raw_response.parse() except Exception as ex: self._handle_request_error(ex) chat_response = self._parse_response_from_openai(response, options=validated_options) + self._attach_served_model_header(chat_response, raw_response.headers) # Once the background response completes, drop the continuation_token from # the caller's options dict. FunctionInvocationLayer reuses the same dict # across tool-loop iterations, so leaving it in place makes the next iteration @@ -680,15 +706,33 @@ class RawOpenAIChatClient( # type: ignore[misc] client, run_options, validated_options = await self._prepare_request(messages, options) try: if "text_format" in run_options: - response = await client.responses.parse(stream=False, **run_options) + raw_response = await client.responses.with_raw_response.parse(stream=False, **run_options) else: - response = await client.responses.create(stream=False, **run_options) + raw_response = await client.responses.with_raw_response.create(stream=False, **run_options) + response = raw_response.parse() except Exception as ex: self._handle_request_error(ex) - return self._parse_response_from_openai(response, options=validated_options) + chat_response = self._parse_response_from_openai(response, options=validated_options) + self._attach_served_model_header(chat_response, raw_response.headers) + return chat_response return _get_response() + @classmethod + def _attach_served_model_header(cls, chat_response: ChatResponse, headers: Any) -> None: + """Surface the ``x-ms-served-model`` response header on the ChatResponse when present.""" + served_model = cls._extract_served_model(headers) + if served_model is None: + return + chat_response.additional_properties[cls.SERVED_MODEL_HEADER] = served_model + + @classmethod + def _extract_served_model(cls, headers: Any) -> str | None: + """Return the ``x-ms-served-model`` response header value when present.""" + if headers is None: + return None + return headers.get(cls.SERVED_MODEL_HEADER) + def _prepare_response_and_text_format( self, *, @@ -1429,9 +1473,10 @@ class RawOpenAIChatClient( # type: ignore[misc] props = content.additional_properties or {} # Local-shell variant serializes as `local_shell_call` carrying a server-issued id; # plain function_call_output pairs by call_id and is safe under storage. - if ( - props.get(OPENAI_SHELL_OUTPUT_TYPE_KEY) == OPENAI_SHELL_OUTPUT_TYPE_LOCAL_SHELL_CALL - and props.get(OPENAI_LOCAL_SHELL_CALL_ITEM_ID_KEY) + if props.get( + OPENAI_SHELL_OUTPUT_TYPE_KEY + ) == OPENAI_SHELL_OUTPUT_TYPE_LOCAL_SHELL_CALL and props.get( + OPENAI_LOCAL_SHELL_CALL_ITEM_ID_KEY ): continue new_args: dict[str, Any] = {} diff --git a/python/packages/openai/tests/openai/test_openai_chat_client.py b/python/packages/openai/tests/openai/test_openai_chat_client.py index 325986a730..7fb6f3139f 100644 --- a/python/packages/openai/tests/openai/test_openai_chat_client.py +++ b/python/packages/openai/tests/openai/test_openai_chat_client.py @@ -72,9 +72,10 @@ class OutputStruct(BaseModel): class _FakeAsyncEventStream: - def __init__(self, events: list[object]) -> None: + def __init__(self, events: list[object], headers: dict[str, str] | None = None) -> None: self._events = events self._iterator = iter(()) + self._headers = headers or {} def __aiter__(self) -> "_FakeAsyncEventStream": self._iterator = iter(self._events) @@ -86,6 +87,32 @@ class _FakeAsyncEventStream: except StopIteration as exc: raise StopAsyncIteration from exc + # The chat client now consumes the streaming response via ``with_raw_response``, + # which returns a wrapper exposing ``.parse()`` (the underlying iterable) and + # ``.headers``. Mimic that interface so test mocks remain a single object. + def parse(self) -> "_FakeAsyncEventStream": + return self + + @property + def headers(self) -> dict[str, str]: + return self._headers + + +def _as_raw(mock_response: MagicMock) -> MagicMock: + """Make ``mock_response`` look like an OpenAI ``with_raw_response`` wrapper. + + The chat client now calls ``responses.with_raw_response.{create,parse,retrieve}`` + and then ``.parse()`` on the returned wrapper to get the actual response payload, + plus ``.headers`` to surface the ``x-ms-served-model`` Azure header. Tests still + patch the underlying ``responses.{create,parse,retrieve}`` methods (the SDK's + raw-response wrapper internally delegates to these), so the patched return value + is what our code unwraps. Setting ``mock_response.parse`` to return the mock + itself lets the existing assertions on ``mock_response.id`` etc. continue to work. + """ + mock_response.parse = MagicMock(return_value=mock_response) + mock_response.headers = {} + return mock_response + class _FakeAsyncEventStreamContext(_FakeAsyncEventStream): async def __aenter__(self) -> "_FakeAsyncEventStreamContext": @@ -477,7 +504,7 @@ async def test_response_format_parse_path() -> None: mock_parsed_response.finish_reason = None mock_parsed_response.conversation = None # No conversation object - with patch.object(client.client.responses, "parse", return_value=mock_parsed_response): + with patch.object(client.client.responses, "parse", return_value=_as_raw(mock_parsed_response)): response = await client.get_response( messages=[Message(role="user", contents=["Test message"])], options={"response_format": OutputStruct, "store": True}, @@ -504,7 +531,7 @@ async def test_response_format_parse_path_with_conversation_id() -> None: mock_parsed_response.conversation = MagicMock() mock_parsed_response.conversation.id = "conversation_456" - with patch.object(client.client.responses, "parse", return_value=mock_parsed_response): + with patch.object(client.client.responses, "parse", return_value=_as_raw(mock_parsed_response)): response = await client.get_response( messages=[Message(role="user", contents=["Test message"])], options={"response_format": OutputStruct, "store": True}, @@ -542,7 +569,7 @@ async def test_response_format_dict_parse_path() -> None: mock_message_item.content = [mock_message_content] mock_response.output = [mock_message_item] - with patch.object(client.client.responses, "create", return_value=mock_response): + with patch.object(client.client.responses, "create", return_value=_as_raw(mock_response)): response = await client.get_response( messages=[Message(role="user", contents=["Test message"])], options={"response_format": response_format}, @@ -554,6 +581,171 @@ async def test_response_format_dict_parse_path() -> None: assert response.value["answer"] == "Parsed" +async def test_served_model_header_captured_on_response() -> None: + """The ``x-ms-served-model`` Azure response header should be surfaced on ChatResponse.additional_properties.""" + from agent_framework.observability import AZURE_OPENAI_SERVED_MODEL_HEADER + + client = OpenAIChatClient(model="test-model", api_key="test-key") + + mock_response = MagicMock() + mock_response.id = "response_123" + mock_response.model = "test-model" + mock_response.created_at = 1000000000 + mock_response.metadata = {} + mock_response.output_parsed = None + mock_response.output = [] + mock_response.usage = None + mock_response.finish_reason = None + mock_response.conversation = None + mock_response.status = "completed" + + raw = _as_raw(mock_response) + raw.headers = {AZURE_OPENAI_SERVED_MODEL_HEADER: "gpt-4o-2024-08-06"} + + with patch.object(client.client.responses, "create", return_value=raw): + response = await client.get_response( + messages=[Message(role="user", contents=["Test message"])], + ) + + assert response.additional_properties.get(AZURE_OPENAI_SERVED_MODEL_HEADER) == "gpt-4o-2024-08-06" + + +async def test_served_model_header_absent_does_not_set_property() -> None: + """When the served-model header is missing the key should not appear on additional_properties.""" + from agent_framework.observability import AZURE_OPENAI_SERVED_MODEL_HEADER + + client = OpenAIChatClient(model="test-model", api_key="test-key") + + mock_response = MagicMock() + mock_response.id = "response_123" + mock_response.model = "test-model" + mock_response.created_at = 1000000000 + mock_response.metadata = {} + mock_response.output_parsed = None + mock_response.output = [] + mock_response.usage = None + mock_response.finish_reason = None + mock_response.conversation = None + mock_response.status = "completed" + + # _as_raw sets headers to {} by default — i.e. no x-ms-served-model. + with patch.object(client.client.responses, "create", return_value=_as_raw(mock_response)): + response = await client.get_response( + messages=[Message(role="user", contents=["Test message"])], + ) + + assert AZURE_OPENAI_SERVED_MODEL_HEADER not in response.additional_properties + + +async def test_served_model_header_captured_on_parse_path() -> None: + """The served-model header should also be captured on the structured-output (parse) path.""" + from agent_framework.observability import AZURE_OPENAI_SERVED_MODEL_HEADER + + client = OpenAIChatClient(model="test-model", api_key="test-key") + + mock_parsed_response = MagicMock() + mock_parsed_response.id = "parsed_response_123" + mock_parsed_response.text = "Parsed response" + mock_parsed_response.model = "test-model" + mock_parsed_response.created_at = 1000000000 + mock_parsed_response.metadata = {} + mock_parsed_response.output_parsed = None + mock_parsed_response.usage = None + mock_parsed_response.finish_reason = None + mock_parsed_response.conversation = None + + raw = _as_raw(mock_parsed_response) + raw.headers = {AZURE_OPENAI_SERVED_MODEL_HEADER: "gpt-4o-2024-08-06"} + + with patch.object(client.client.responses, "parse", return_value=raw): + response = await client.get_response( + messages=[Message(role="user", contents=["Test message"])], + options={"response_format": OutputStruct, "store": True}, + ) + + assert response.additional_properties.get(AZURE_OPENAI_SERVED_MODEL_HEADER) == "gpt-4o-2024-08-06" + + +async def test_served_model_header_propagated_to_streaming_updates() -> None: + """In streaming mode the served-model header should be set on every ChatResponseUpdate.""" + from agent_framework.observability import AZURE_OPENAI_SERVED_MODEL_HEADER + + client = OpenAIChatClient(model="test-model", api_key="test-key") + + events = [ + ResponseTextDeltaEvent( + type="response.output_text.delta", + content_index=0, + item_id="text_item", + output_index=0, + sequence_number=1, + logprobs=[], + delta="Hello", + ), + ResponseTextDeltaEvent( + type="response.output_text.delta", + content_index=0, + item_id="text_item", + output_index=0, + sequence_number=2, + logprobs=[], + delta=" world", + ), + ] + + fake_stream = _FakeAsyncEventStream( + events, + headers={AZURE_OPENAI_SERVED_MODEL_HEADER: "gpt-4o-2024-08-06"}, + ) + + with ( + patch.object(client, "_prepare_request", new=AsyncMock(return_value=(client.client, {}, {}))), + patch.object(client.client.responses, "create", new=AsyncMock(return_value=fake_stream)), + patch.object(client, "_get_metadata_from_response", return_value={}), + ): + stream = client._inner_get_response(messages=[Message(role="user", contents=["Hi"])], options={}, stream=True) + updates = [update async for update in stream] + + assert updates, "Expected at least one streaming update" + for update in updates: + assert update.additional_properties is not None + assert update.additional_properties.get(AZURE_OPENAI_SERVED_MODEL_HEADER) == "gpt-4o-2024-08-06" + + +async def test_served_model_header_absent_in_streaming_updates() -> None: + """When the header is missing in streaming mode it should not be added to update properties.""" + from agent_framework.observability import AZURE_OPENAI_SERVED_MODEL_HEADER + + client = OpenAIChatClient(model="test-model", api_key="test-key") + + events = [ + ResponseTextDeltaEvent( + type="response.output_text.delta", + content_index=0, + item_id="text_item", + output_index=0, + sequence_number=1, + logprobs=[], + delta="Hello", + ), + ] + + fake_stream = _FakeAsyncEventStream(events) # default empty headers + + with ( + patch.object(client, "_prepare_request", new=AsyncMock(return_value=(client.client, {}, {}))), + patch.object(client.client.responses, "create", new=AsyncMock(return_value=fake_stream)), + patch.object(client, "_get_metadata_from_response", return_value={}), + ): + stream = client._inner_get_response(messages=[Message(role="user", contents=["Hi"])], options={}, stream=True) + updates = [update async for update in stream] + + assert updates, "Expected at least one streaming update" + for update in updates: + if update.additional_properties is not None: + assert AZURE_OPENAI_SERVED_MODEL_HEADER not in update.additional_properties + + async def test_bad_request_error_non_content_filter() -> None: """Test get_response BadRequestError without content_filter.""" client = OpenAIChatClient(model="test-model", api_key="test-key") @@ -953,7 +1145,9 @@ async def test_local_shell_tool_is_invoked_in_function_loop() -> None: mock_text_item.content = [mock_text_content] mock_response2.output = [mock_text_item] - with patch.object(client.client.responses, "create", side_effect=[mock_response1, mock_response2]) as mock_create: + with patch.object( + client.client.responses, "create", side_effect=[_as_raw(mock_response1), _as_raw(mock_response2)] + ) as mock_create: await client.get_response( messages=[Message(role="user", contents=["What Python version is available?"])], options={"tools": [local_shell_tool]}, @@ -1026,7 +1220,9 @@ async def test_shell_call_is_invoked_as_local_shell_function_loop() -> None: mock_text_item.content = [mock_text_content] mock_response2.output = [mock_text_item] - with patch.object(client.client.responses, "create", side_effect=[mock_response1, mock_response2]) as mock_create: + with patch.object( + client.client.responses, "create", side_effect=[_as_raw(mock_response1), _as_raw(mock_response2)] + ) as mock_create: await client.get_response( messages=[Message(role="user", contents=["What Python version is available?"])], options={"tools": [local_shell_tool]}, @@ -1097,7 +1293,9 @@ async def test_tool_loop_store_false_omits_reasoning_items_from_second_request() mock_text_item.content = [mock_text_content] mock_response2.output = [mock_text_item] - with patch.object(client.client.responses, "create", side_effect=[mock_response1, mock_response2]) as mock_create: + with patch.object( + client.client.responses, "create", side_effect=[_as_raw(mock_response1), _as_raw(mock_response2)] + ) as mock_create: response = await client.get_response( messages=[Message(role="user", contents=["What's the weather in Amsterdam?"])], options={ @@ -2810,7 +3008,9 @@ async def test_end_to_end_mcp_approval_flow(span_exporter) -> None: mock_response2.output = [mock_text_item] # Patch the create call to return the two mocked responses in sequence - with patch.object(client.client.responses, "create", side_effect=[mock_response1, mock_response2]) as mock_create: + with patch.object( + client.client.responses, "create", side_effect=[_as_raw(mock_response1), _as_raw(mock_response2)] + ) as mock_create: # First call: get the approval request response = await client.get_response(messages=[Message(role="user", contents=["Trigger approval"])]) assert response.messages[0].contents[0].type == "function_approval_request" @@ -3235,7 +3435,7 @@ async def test_inner_get_response_streaming_with_response_format_tracks_reasonin "_prepare_request", new=AsyncMock(return_value=(client.client, {"text_format": OutputStruct}, {})), ), - patch.object(client.client.responses, "stream", return_value=_FakeAsyncEventStreamContext(events)), + patch.object(client.client.responses, "create", new=AsyncMock(return_value=_FakeAsyncEventStream(events))), patch.object(client, "_get_metadata_from_response", return_value={}), ): stream = client._inner_get_response(messages=messages, options={}, stream=True) @@ -4120,9 +4320,7 @@ async def test_prepare_options_with_conversation_id_strips_server_items_for_mixe types = [item.get("type") for item in options["input"]] assert "reasoning" not in types assert "function_call" not in types - output_call_ids = { - item["call_id"] for item in options["input"] if item.get("type") == "function_call_output" - } + output_call_ids = {item["call_id"] for item in options["input"] if item.get("type") == "function_call_output"} assert output_call_ids == {"call_history", "call_live"} assert options["previous_response_id"] == "resp_prev123"