Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions sdk/evaluation/azure-ai-evaluation/dev_requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,4 @@ promptflow-core>=1.17.1
promptflow-devkit>=1.17.1
# Note: redteam extra (pyrit) is installed separately via InjectedPackages in platform-matrix.json
# to avoid pillow version conflicts with promptflow-devkit (pillow<11 vs >=12.1)
# Test-only openai pin (see below). Not applied to install_requires so end-user
# installs can still use openai>=3.0 features (Responses API, workload identity,
# etc.) that are runtime-compatible with this SDK. The pin is here because
# tests/__openai_patcher.py constructs httpx.URL objects, but openai 3.x's
# client requires httpx2.URL — a mismatch that only triggers inside the test
# proxy path, never in real production code. Remove this pin once
# tests/__openai_patcher.py is updated for httpx2 compatibility.
openai<3.0
../azure-ai-evaluation

40 changes: 17 additions & 23 deletions sdk/evaluation/azure-ai-evaluation/tests/__openai_patcher.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
"""Implementation of an httpx.Client that forwards traffic to the Azure SDK test-proxy.
"""Implementation of an OpenAI HTTP client that forwards traffic to the Azure SDK test-proxy.

.. note::

This module has side-effects!

Importing this module will replace the default httpx.Client used
by the openai package with one that can redirect it's traffic
to the Azure SDK test-proxy on demand.
Importing this module replaces OpenAI's default sync and async HTTP
wrapper classes with proxy-aware subclasses. The subclasses preserve
whichever underlying transport OpenAI selected, including httpx2.

"""

Expand Down Expand Up @@ -69,14 +67,16 @@ def _reroute_to_proxy(self, request: httpx.Request) -> Iterator[None]:
assert self.is_recording(), f"{self._reroute_to_proxy.__qualname__} should only be called while recording"
config = self.recording_config
original_url = request.url
url_type = type(original_url)
Comment thread
slister1001 marked this conversation as resolved.

request_path = original_url.copy_with(scheme="", netloc=b"")
request.url = httpx.URL(config.proxy_url).join(request_path)
request.url = url_type(config.proxy_url).join(request_path)

original_headers = request.headers
request.headers = request.headers.copy()
request.headers.setdefault(
"x-recording-upstream-base-uri", str(httpx.URL(scheme=original_url.scheme, netloc=original_url.netloc))
"x-recording-upstream-base-uri",
str(url_type(scheme=original_url.scheme, netloc=original_url.netloc)),
)
request.headers["x-recording-id"] = config.recording_id
request.headers["x-recording-mode"] = config.recording_mode
Expand All @@ -89,30 +89,24 @@ def _reroute_to_proxy(self, request: httpx.Request) -> Iterator[None]:

class TestProxyHttpxClient(TestProxyHttpxClientBase, openai._base_client.SyncHttpxClientWrapper):
@override
def send(self, request: httpx.Request, **kwargs) -> httpx.Response:
def _send_single_request(self, request: httpx.Request) -> httpx.Response:
# OpenAI auth and redirect-safety hooks must evaluate the original Azure URL before proxy routing.
if self.is_recording():
with self._reroute_to_proxy(request):
response = super().send(request, **kwargs)

response.request.url = request.url
return response
else:
return super().send(request, **kwargs)
return super()._send_single_request(request)
return super()._send_single_request(request)


class TestProxyAsyncHttpxClient(TestProxyHttpxClientBase, openai._base_client.AsyncHttpxClientWrapper):
@override
async def send(self, request: httpx.Request, **kwargs) -> httpx.Response:
async def _send_single_request(self, request: httpx.Request) -> httpx.Response:
# OpenAI auth and redirect-safety hooks must evaluate the original Azure URL before proxy routing.
if self.is_recording():
with self._reroute_to_proxy(request):
response = await super().send(request, **kwargs)

response.request.url = request.url
return response
else:
return await super().send(request, **kwargs)
return await super()._send_single_request(request)
return await super()._send_single_request(request)


# openai._base_client.{Async,Sync}HttpxClientWrapper are default httpx.Clients instantiated by openai
# OpenAI instantiates these aliases when no custom HTTP client is provided.
openai._base_client.SyncHttpxClientWrapper = TestProxyHttpxClient
openai._base_client.AsyncHttpxClientWrapper = TestProxyAsyncHttpxClient
1 change: 1 addition & 0 deletions sdk/evaluation/azure-ai-evaluation/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ def evaluatation_run_sanitizer() -> None:

# removes some headers since they are causing some unnecessary mismatches in recordings
headers_to_ignore = [
"accept-encoding",
"ms-azure-ai-promptflow",
"ms-azure-ai-promptflow-called-from",
"x-ms-useragent",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1782,9 +1782,10 @@ def test_prompty_evaluator(

expected_user_agent = f"{base_user_agent} {added_useragent}"

from httpx import AsyncClient, Request
from openai import _base_client as openai_base_client

with self._transparent_mock_method(AsyncClient, "send") as mock: # OpenAI requests sent with httpx
# __openai_patcher replaces this alias at import time; patching the alias is transport-agnostic.
with self._transparent_mock_method(openai_base_client.AsyncHttpxClientWrapper, "send") as mock:
evaluator = evaluator_cls(user_agent_model_config)

with UserAgentSingleton.add_useragent_product(added_useragent):
Expand All @@ -1794,7 +1795,6 @@ def test_prompty_evaluator(

for call_args in mock.call_args_list:
_, request, *_ = call_args.args
request: Request

# Not checking for strict equality because some evaluators add to the user agent
assert expected_user_agent in request.headers["User-Agent"]
Original file line number Diff line number Diff line change
Expand Up @@ -548,9 +548,10 @@ def test_evaluate_user_agent(self, user_agent_model_config: AzureOpenAIModelConf

expected_user_agent = f"{base_user_agent} {added_useragent}"

from httpx import AsyncClient, Request
from openai import _base_client as openai_base_client

with self._transparent_mock_method(AsyncClient, "send") as mock:
# __openai_patcher replaces this alias at import time; patching the alias is transport-agnostic.
with self._transparent_mock_method(openai_base_client.AsyncHttpxClientWrapper, "send") as mock:
evaluate(
data=data_file,
evaluators={"fluency": FluencyEvaluator(user_agent_model_config)},
Expand All @@ -561,7 +562,6 @@ def test_evaluate_user_agent(self, user_agent_model_config: AzureOpenAIModelConf

for call_args in mock.call_args_list:
_, request, *_ = call_args.args
request: Request

# Not checking for strict equality because some evaluators add to the user agent
assert expected_user_agent in request.headers["User-Agent"]