mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-28 06:23:07 +00:00
test(azure_ai): cover Foundry agents on the project Responses API
This commit is contained in:
parent
e333ccabdb
commit
f5d65b8c06
1 changed files with 227 additions and 1 deletions
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue