diff --git a/sdk/voicelive/azure-ai-voicelive/CHANGELOG.md b/sdk/voicelive/azure-ai-voicelive/CHANGELOG.md index 5ebd5c999a9c..e75018f44514 100644 --- a/sdk/voicelive/azure-ai-voicelive/CHANGELOG.md +++ b/sdk/voicelive/azure-ai-voicelive/CHANGELOG.md @@ -1,5 +1,12 @@ # Release History +## 1.3.1 (Unreleased) + +### Other Changes + +- The SDK now identifies Voice Live WebSocket connections through the `User-Agent` header and + `x-ms-client-sdk` query parameter. + ## 1.3.0 (2026-08-03) ### Features Added diff --git a/sdk/voicelive/azure-ai-voicelive/azure/ai/voicelive/aio/_patch.py b/sdk/voicelive/azure-ai-voicelive/azure/ai/voicelive/aio/_patch.py index 69a99f13629e..4e8801ed3b44 100644 --- a/sdk/voicelive/azure-ai-voicelive/azure/ai/voicelive/aio/_patch.py +++ b/sdk/voicelive/azure-ai-voicelive/azure/ai/voicelive/aio/_patch.py @@ -42,7 +42,9 @@ from azure.core.credentials import AzureKeyCredential from azure.core.credentials_async import AsyncTokenCredential from azure.core.exceptions import AzureError +from azure.core.pipeline.policies import UserAgentPolicy from azure.ai.voicelive.models import ClientEvent, ServerEvent, RequestSession +from azure.ai.voicelive._version import VERSION # === Local === @@ -67,6 +69,7 @@ ] log = logging.getLogger(__name__) +_USER_AGENT = UserAgentPolicy(sdk_moniker=f"ai-voicelive/{VERSION}").user_agent def _json_default(o: Any) -> Any: @@ -775,7 +778,7 @@ async def __aenter__(self) -> VoiceLiveConnection: self.__connection_options.setdefault("heartbeat", 30) auth_headers = await self._get_auth_headers() - headers = {**auth_headers, **dict(self.__extra_headers)} + headers = {"User-Agent": _USER_AGENT, **auth_headers, **dict(self.__extra_headers)} session = aiohttp.ClientSession() try: @@ -821,7 +824,10 @@ def _prepare_url(self) -> str: else ("ws" if parsed.scheme.startswith("http") else parsed.scheme) ) - params: dict[str, Any] = {"api-version": self.__api_version} + params: dict[str, Any] = { + "api-version": self.__api_version, + "x-ms-client-sdk": _USER_AGENT, + } if self.__model is not None: params["model"] = self.__model diff --git a/sdk/voicelive/azure-ai-voicelive/tests/unit/test_unit_connection.py b/sdk/voicelive/azure-ai-voicelive/tests/unit/test_unit_connection.py index ec121ca6a57d..0b3f257fdcd8 100644 --- a/sdk/voicelive/azure-ai-voicelive/tests/unit/test_unit_connection.py +++ b/sdk/voicelive/azure-ai-voicelive/tests/unit/test_unit_connection.py @@ -6,6 +6,7 @@ import pytest from unittest.mock import AsyncMock, patch +from urllib.parse import parse_qs, urlparse pytest.importorskip( "aiohttp", @@ -21,6 +22,7 @@ connect, ) from azure.ai.voicelive.aio._patch import _VoiceLiveConnectionManager +from azure.ai.voicelive._version import VERSION from azure.ai.voicelive.models import ( ClientEventSessionUpdate, ClientEventResponseCreate, @@ -208,6 +210,40 @@ async def test_response_resource_creation(self): assert connection.response._connection is connection +@pytest.mark.asyncio +class TestConnectionIdentification: + """Test SDK identification on WebSocket connections.""" + + async def _connect_and_get_headers(self, headers=None): + with patch("azure.ai.voicelive.aio._patch.aiohttp.ClientSession") as mock_client_session: + session = mock_client_session.return_value + session.ws_connect = AsyncMock(return_value=AsyncMock()) + session.close = AsyncMock() + + async with connect( + credential=AzureKeyCredential("test-key"), + endpoint="wss://test-endpoint.com", + model="gpt-realtime", + headers=headers, + ): + pass + + return session.ws_connect.await_args.kwargs["headers"] + + async def test_connection_uses_sdk_user_agent(self): + """Test the default User-Agent identifies the package and version.""" + headers = await self._connect_and_get_headers() + + assert "azsdk-python-ai-voicelive" in headers["User-Agent"] + assert VERSION in headers["User-Agent"] + + async def test_connection_preserves_caller_user_agent(self): + """Test a caller-supplied User-Agent overrides the SDK default.""" + headers = await self._connect_and_get_headers({"User-Agent": "custom-user-agent"}) + + assert headers["User-Agent"] == "custom-user-agent" + + class TestVoiceLiveConnectionIntegration: """Integration tests for VoiceLiveConnection.""" @@ -699,3 +735,33 @@ def test_url_uses_default_api_version(self): url = manager._prepare_url() assert "api-version=2026-07-15" in url + + def test_url_includes_sdk_identifier(self): + """Test that the connection URL identifies the SDK.""" + manager = _VoiceLiveConnectionManager( + credential=self.credential, + endpoint=self.endpoint, + agent_config=None, + extra_query={}, + extra_headers={}, + ) + + query = parse_qs(urlparse(manager._prepare_url()).query) + sdk_identifier = query["x-ms-client-sdk"][0] + + assert "azsdk-python-ai-voicelive" in sdk_identifier + assert VERSION in sdk_identifier + + def test_url_preserves_traffic_type(self): + """Test that SDK identification does not overwrite customer traffic tagging.""" + manager = _VoiceLiveConnectionManager( + credential=self.credential, + endpoint=f"{self.endpoint}?trafficType=customer-tag", + agent_config=None, + extra_query={}, + extra_headers={}, + ) + + query = parse_qs(urlparse(manager._prepare_url()).query) + + assert query["trafficType"] == ["customer-tag"]