test(azure_ai): cover Foundry agents on the project Responses API

This commit is contained in:
mateo-berri 2026-10-20 18:15:24 -07:00
parent e333ccabdb
commit f5d65b8c06

View file

@ -1,5 +1,6 @@
import json
from typing import Final
from unittest.mock import patch
import httpx
import pytest
@ -7,6 +8,7 @@ import respx
import litellm
from litellm.llms.azure.responses.transformation import AzureOpenAIResponsesAPIConfig
from litellm.llms.azure_ai.common_utils import AZURE_AI_AGENTS_SCOPE
from litellm.llms.azure_ai.responses.transformation import AzureAIResponsesAPIConfig
from litellm.responses.main import _will_bridge_to_chat_completions
from litellm.types.llms.openai import ResponsesAPIOptionalRequestParams
@ -103,7 +105,7 @@ def test_other_hosts_keep_chat_bridge(model, api_base):
assert config is None
@pytest.mark.parametrize("model", ["claude-3-5-sonnet", "model_router/gpt-5", "agents/my-agent"])
@pytest.mark.parametrize("model", ["claude-3-5-sonnet", "model_router/gpt-5", "agents/asst_abc123"])
def test_non_openai_surfaces_keep_chat_bridge(model):
config = ProviderConfigManager.get_provider_responses_api_config(
provider="azure_ai", model=model, api_base=FOUNDRY_PROJECT_BASE
@ -383,3 +385,227 @@ def test_foundry_responses_none_unlocks_temperature_off_the_azure_ai_row(
assert params["temperature"] == 0.2
assert params["reasoning"] == {"effort": "none"}
@pytest.mark.asyncio
@respx.mock
@pytest.mark.parametrize(
"model,expected_reference",
[
("azure_ai/agents/support-agent", {"type": "agent_reference", "name": "support-agent"}),
("azure_ai/agents/support-agent:3", {"type": "agent_reference", "name": "support-agent", "version": "3"}),
],
)
async def test_aresponses_sends_agent_reference_instead_of_model(model, expected_reference):
route = respx.post(url__regex=r".*/openai/v1/responses(\?.*)?$").mock(
return_value=httpx.Response(200, json=_responses_payload("gpt-4.1"))
)
await litellm.aresponses(
model=model, input="What is 2+2?", max_output_tokens=50, api_base=FOUNDRY_PROJECT_BASE, api_key="fake-key"
)
request = route.calls.last.request
body = json.loads(request.content)
assert f"{request.url.scheme}://{request.url.host}{request.url.path}" == FOUNDRY_RESPONSES_URL
assert body["agent_reference"] == expected_reference
assert "model" not in body
assert body["input"] == "What is 2+2?"
assert body["max_output_tokens"] == 50
@pytest.mark.asyncio
@respx.mock
async def test_router_aresponses_keeps_the_deployment_agent_over_extra_body():
route = respx.post(url__regex=r".*/openai/v1/responses(\?.*)?$").mock(
return_value=httpx.Response(200, json=_responses_payload("gpt-4.1"))
)
router = litellm.Router(
model_list=[
{
"model_name": "support-agent",
"litellm_params": {
"model": "azure_ai/agents/support-agent",
"api_base": FOUNDRY_PROJECT_BASE,
"api_key": "fake-key",
},
}
],
num_retries=0,
)
await router.aresponses(
model="support-agent",
input="What is 2+2?",
extra_body={"agent_reference": {"type": "agent_reference", "name": "billing-agent"}, "store": False},
)
body = json.loads(route.calls.last.request.content)
assert body["agent_reference"] == {"type": "agent_reference", "name": "support-agent"}
assert body["store"] is False
assert "model" not in body
AGENT_OWNED_PARAMS = {
"instructions": "Answer in French",
"temperature": 0.2,
"top_p": 0.5,
"tools": [WEATHER_TOOL],
"text": {"format": {"type": "text"}},
"reasoning": {"effort": "high"},
}
@pytest.mark.asyncio
@respx.mock
async def test_aresponses_drops_agent_owned_params_under_drop_params():
route = respx.post(url__regex=r".*/openai/v1/responses(\?.*)?$").mock(
return_value=httpx.Response(200, json=_responses_payload("gpt-4.1"))
)
await litellm.aresponses(
model="azure_ai/agents/support-agent",
input="What is 2+2?",
tool_choice="auto",
drop_params=True,
api_base=FOUNDRY_PROJECT_BASE,
api_key="fake-key",
**AGENT_OWNED_PARAMS,
)
body = json.loads(route.calls.last.request.content)
assert body.keys().isdisjoint(AGENT_OWNED_PARAMS)
assert body["tool_choice"] == "auto"
@pytest.mark.asyncio
@respx.mock
async def test_aresponses_reaches_the_agent_when_the_deployment_declares_reasoning_efforts():
route = respx.post(url__regex=r".*/openai/v1/responses(\?.*)?$").mock(
return_value=httpx.Response(200, json=_responses_payload("gpt-4.1"))
)
await litellm.aresponses(
model="azure_ai/agents/support-agent",
input="What is 2+2?",
max_output_tokens=50,
api_base=FOUNDRY_PROJECT_BASE,
api_key="fake-key",
model_info={"reasoning_effort_levels": ["low", "medium", "high"]},
)
body = json.loads(route.calls.last.request.content)
assert body["agent_reference"] == {"type": "agent_reference", "name": "support-agent"}
assert body["max_output_tokens"] == 50
@pytest.mark.asyncio
@respx.mock
@pytest.mark.parametrize("param", sorted(AGENT_OWNED_PARAMS))
async def test_aresponses_rejects_agent_owned_params_without_drop_params(param):
route = respx.post(url__regex=r".*/openai/v1/responses(\?.*)?$")
with pytest.raises(litellm.UnsupportedParamsError, match=param):
await litellm.aresponses(
model="azure_ai/agents/support-agent",
input="What is 2+2?",
api_base=FOUNDRY_PROJECT_BASE,
api_key="fake-key",
**{param: AGENT_OWNED_PARAMS[param]},
)
assert not route.called
@pytest.mark.asyncio
@respx.mock
async def test_aresponses_streams_agent_answers_from_foundry():
completed = _responses_payload("gpt-4.1")
stream_body = "".join(
f"event: {event['type']}\ndata: {json.dumps(event)}\n\n"
for event in (
{"type": "response.created", "response": {**completed, "status": "in_progress"}},
{"type": "response.output_text.delta", "item_id": "msg_1", "output_index": 0, "content_index": 0, "delta": "4"},
{"type": "response.completed", "response": completed},
)
)
route = respx.post(url__regex=r".*/openai/v1/responses(\?.*)?$").mock(
return_value=httpx.Response(200, text=stream_body, headers={"content-type": "text/event-stream"})
)
stream = await litellm.aresponses(
model="azure_ai/agents/support-agent",
input="What is 2+2?",
stream=True,
api_base=FOUNDRY_PROJECT_BASE,
api_key="fake-key",
)
events = [event async for event in stream]
assert json.loads(route.calls.last.request.content)["stream"] is True
assert [event.type for event in events] == ["response.created", "response.output_text.delta", "response.completed"]
assert events[1].delta == "4"
@pytest.mark.asyncio
@respx.mock
@pytest.mark.parametrize(
"configured_scope,expected_scope",
[(None, AZURE_AI_AGENTS_SCOPE), ("api://custom-app/.default", "api://custom-app/.default")],
)
async def test_aresponses_mints_the_service_principal_token_for_the_agents_scope(configured_scope, expected_scope):
route = respx.post(url__regex=r".*/openai/v1/responses(\?.*)?$").mock(
return_value=httpx.Response(200, json=_responses_payload("gpt-4.1"))
)
with patch("litellm.llms.azure.common_utils.get_azure_ad_token_from_entra_id") as mock_entra_id: # test-quality-ok: stubs the Entra token fetch to assert the scope an agent entry requests and the bearer it sends; live service principal path proven by the PR's Foundry e2e QA
mock_entra_id.return_value = lambda: "agent-sp-token"
await litellm.aresponses(
model="azure_ai/agents/support-agent",
input="What is 2+2?",
api_base=FOUNDRY_PROJECT_BASE,
tenant_id="tenant",
client_id="client",
client_secret="secret",
azure_scope=configured_scope,
)
mock_entra_id.assert_called_once_with(
tenant_id="tenant", client_id="client", client_secret="secret", scope=expected_scope
)
assert route.calls.last.request.headers["Authorization"] == "Bearer agent-sp-token"
def _agent_answer_payload(text: str) -> dict:
return {
**_responses_payload("gpt-4.1"),
"output": [
{
"type": "message",
"id": "msg_1",
"status": "completed",
"role": "assistant",
"content": [{"type": "output_text", "text": text, "annotations": []}],
}
],
}
@pytest.mark.asyncio
@respx.mock
async def test_acompletion_reaches_the_agent_through_the_project_responses_api():
route = respx.post(url__regex=r".*/openai/v1/responses(\?.*)?$").mock(
return_value=httpx.Response(200, json=_agent_answer_payload("4"))
)
response = await litellm.acompletion(
model="azure_ai/agents/support-agent",
messages=[{"role": "user", "content": "What is 2+2?"}],
api_base=FOUNDRY_PROJECT_BASE,
api_key="fake-key",
)
body = json.loads(route.calls.last.request.content)
assert body["agent_reference"] == {"type": "agent_reference", "name": "support-agent"}
assert "model" not in body
assert response.choices[0].message.content == "4"