feat(azure_ai): reach Foundry agents through the project Responses API

This commit is contained in:
mateo-berri 2026-10-20 18:35:45 -07:00
parent f5d65b8c06
commit a58f1d620e
5 changed files with 102 additions and 1 deletions

View file

@ -140,6 +140,15 @@ def get_azure_ai_auth_headers(
AZURE_MODEL_ROUTER_SELECTED_MODEL_KEY: Final = "azure_model_router_selected_model"
FOUNDRY_CLASSIC_AGENT_ID_PREFIX: Final = "asst_"
def is_foundry_project_agent(model: str | None) -> bool:
if model is None or AzureFoundryModelInfo.get_azure_ai_route(model) != "agents":
return False
return not model.split("agents/", 1)[1].startswith(FOUNDRY_CLASSIC_AGENT_ID_PREFIX)
def azure_ai_supports_native_responses(model: str | None, api_base: str | None) -> bool:
resolved_base: Final = AzureFoundryModelInfo.get_api_base(api_base)
if resolved_base is not None and not is_azure_openai_v1_host(resolved_base):

View file

@ -1,3 +1,4 @@
from collections.abc import Collection, Mapping
from typing import Final
import httpx
@ -5,15 +6,19 @@ import httpx
from litellm.llms.azure.responses.transformation import AzureOpenAIResponsesAPIConfig
from litellm.llms.azure_ai.chat.transformation import AzureAIGPT5Config
from litellm.llms.azure_ai.common_utils import (
AZURE_AI_AGENTS_SCOPE,
AzureFoundryModelInfo,
api_key_header_for_base,
get_azure_ai_auth_headers,
)
from litellm.types.llms.azure_ai import FoundryAgentReference
from litellm.types.llms.openai import ResponseInputParam, ResponsesAPIOptionalRequestParams
from litellm.types.router import GenericLiteLLMParams
from litellm.types.utils import LlmProviders
_PROJECT_PATH_PREFIX: Final = ("api", "projects")
_RESPONSES_PATH: Final = ("openai", "v1", "responses")
_AGENT_OWNED_PARAMS: Final = frozenset({"instructions", "temperature", "top_p", "tools", "text", "reasoning"})
def _responses_url(api_base: str) -> str:
@ -64,3 +69,74 @@ class AzureAIResponsesAPIConfig(AzureOpenAIResponsesAPIConfig):
"Set the api_base parameter or the AZURE_AI_API_BASE environment variable."
)
return _responses_url(resolved_base)
def _agent_reference(model: str) -> FoundryAgentReference:
name, _, version = model.split("agents/", 1)[1].partition(":")
if version:
return FoundryAgentReference(type="agent_reference", name=name, version=version)
return FoundryAgentReference(type="agent_reference", name=name)
class AzureAIAgentResponsesAPIConfig(AzureAIResponsesAPIConfig):
def validate_environment(
self,
headers: dict[str, str], # mutable-ok: signature fixed by the override contract
model: str,
litellm_params: GenericLiteLLMParams | None,
) -> dict[str, str]: # mutable-ok: same
params: Final = litellm_params or GenericLiteLLMParams()
return super().validate_environment( # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType] # parent returns a bare dict
headers, model, params.model_copy(update={"azure_scope": params.azure_scope or AZURE_AI_AGENTS_SCOPE})
)
def should_fake_stream(
self, model: str | None, stream: bool | None, custom_llm_provider: str | None = None
) -> bool:
return False
def get_supported_openai_params(self, model: str) -> list[str]: # mutable-ok: override contract
inherited: Final[list[str]] = super().get_supported_openai_params(model) # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType] # parent returns a bare list # mutable-ok: same
return [param for param in inherited if param not in _AGENT_OWNED_PARAMS]
def map_openai_params_for_deployment(
self,
response_api_optional_params: ResponsesAPIOptionalRequestParams,
model: str,
drop_params: bool,
*,
declared_reasoning_efforts: Collection[str],
) -> Mapping[str, object]:
mapped: Final = super().map_openai_params_for_deployment(
response_api_optional_params, model, drop_params, declared_reasoning_efforts=declared_reasoning_efforts
)
return {key: value for key, value in mapped.items() if key not in _AGENT_OWNED_PARAMS}
def transform_responses_api_request(
self,
model: str,
input: str | ResponseInputParam,
response_api_optional_request_params: dict[str, object], # mutable-ok: signature fixed by the override contract
litellm_params: GenericLiteLLMParams,
headers: dict[str, str], # mutable-ok: same
) -> dict[str, object]: # mutable-ok: same
request: Final[dict[str, object]] = super().transform_responses_api_request( # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType] # parent returns a bare dict # mutable-ok: same
model=model,
input=input,
response_api_optional_request_params=response_api_optional_request_params,
litellm_params=litellm_params,
headers=headers,
)
return {
**{key: value for key, value in request.items() if key != "model"},
"agent_reference": _agent_reference(model),
}
def transform_extra_body(
self,
extra_body: Mapping[str, object],
request: Mapping[str, object],
model: str,
litellm_params: GenericLiteLLMParams,
) -> Mapping[str, object]:
return {**extra_body, "agent_reference": _agent_reference(model)}

View file

@ -9,6 +9,7 @@ from litellm.llms.azure_ai.chat.transformation import AzureAIGPT5Config
from litellm.llms.azure_ai.common_utils import (
azure_ai_supports_native_responses,
foundry_chat_rejects_function_tools_while_reasoning,
is_foundry_project_agent,
)
from litellm.llms.openai.chat.gpt_5_transformation import OpenAIGPT5Config
from litellm.secret_managers.main import get_secret_str
@ -181,6 +182,9 @@ def responses_api_bridge_check(
elif pinned_mode == "chat" and backend_serves_chat_completions(model_info):
model_info["mode"] = "chat"
if custom_llm_provider == LlmProviders.AZURE_AI.value and is_foundry_project_agent(model):
model_info["mode"] = "responses"
if web_search_options is not None and custom_llm_provider == "xai":
model_info["mode"] = "responses"
model = model.replace("responses/", "")

View file

@ -1,6 +1,6 @@
from typing import Literal
from typing_extensions import Required, TypedDict
from typing_extensions import NotRequired, ReadOnly, Required, TypedDict
class ImageEmbeddingInput(TypedDict, total=False):
@ -15,3 +15,9 @@ class ImageEmbeddingRequest(TypedDict, total=False):
input: Required[list[ImageEmbeddingInput]]
dimensions: int
encoding_format: EncodingFormat
class FoundryAgentReference(TypedDict):
type: ReadOnly[Literal["agent_reference"]]
name: ReadOnly[str]
version: ReadOnly[NotRequired[str]]

View file

@ -9832,8 +9832,14 @@ class ProviderConfigManager:
elif litellm.LlmProviders.AZURE_AI == provider:
from litellm.llms.azure_ai.common_utils import (
azure_ai_supports_native_responses,
is_foundry_project_agent,
)
from litellm.llms.azure_ai.responses.transformation import (
AzureAIAgentResponsesAPIConfig,
)
if is_foundry_project_agent(model):
return AzureAIAgentResponsesAPIConfig()
if azure_ai_supports_native_responses(model, api_base):
return litellm.AzureAIResponsesAPIConfig()
return None