Address review: surface served model via ChatResponse.model

Apply blocking review feedback from PR #5910:

- Use ChatResponse.model / ChatResponseUpdate.model as the source of truth
  for the Azure x-ms-served-model header value, instead of stashing it in
  additional_properties and overriding it again in observability.
  Observability already reads response.model; the chat client now overwrites
  it post-parse when the served-model header is present. Empirically the
  Azure Responses API returns the deployment alias in body.model and the
  actual snapshot (e.g. gpt-5-nano-2025-08-07) in this header.

- Move the AZURE_OPENAI_SERVED_MODEL_HEADER constant out of observability.py
  and into RawOpenAIChatClient (as the SERVED_MODEL_HEADER ClassVar). The
  header is Azure-OpenAI-Responses-API-specific so observability does not
  need to know about it.

- Revert the streaming text_format path to client.responses.stream(...) and
  drop the _pydantic_model_to_text_format_param helper. That helper imported
  from openai.lib._parsing._responses (a private SDK path) and the swap to
  responses.create(stream=True) dropped client-side output_parsed for
  structured-output streaming. The streaming-with-text_format path is the
  only one that does not surface the served-model header - documented inline.

- Wrap the raw streaming responses in async with so the underlying socket
  closes deterministically (continuation_token retrieve + create paths).

- Fix the empty-string / whitespace-only header at the source by stripping
  in _extract_served_model and returning None when nothing remains.

- Revert unrelated formatting-only churn in _skills.py and test_mcp.py.

- Update unit tests to assert against chat_response.model / update.model
  and add an aggregated streaming assertion plus a pin that the
  streaming-with-text_format path does not get the header.

Verified end-to-end against Azure OpenAI Responses API: deployment alias
gpt-5-nano now reports gpt-5-nano-2025-08-07 as ChatResponse.model in both
the non-streaming and streaming paths.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Eduard van Valkenburg
2026-05-19 07:42:27 +02:00
Unverified
parent 1b72c8c0da
commit cb8249ed95
4 changed files with 195 additions and 230 deletions
@@ -1739,78 +1739,6 @@ 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
@@ -2553,28 +2481,6 @@ 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.