mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
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:
@@ -101,12 +101,6 @@ 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
|
||||
@@ -2131,14 +2125,6 @@ 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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user