fix(azure): match deployment segments case-insensitively and keep relay helpers immutable

This commit is contained in:
mateo-berri 2026-09-08 17:05:06 -07:00
parent 5e056a264e
commit a892e67c40
4 changed files with 30 additions and 13 deletions

View file

@ -1,5 +1,6 @@
import re
from collections.abc import Callable, Collection, Mapping, Sequence
from types import MappingProxyType
from typing import TYPE_CHECKING, Final, Optional
import httpx
@ -55,9 +56,7 @@ def logged_responses_stream(all_chunks: Sequence[str], logging_obj: Logging) ->
"""A streaming logging object assembles the logged response from the terminal event, not from its body."""
from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig
terminal_event: Final = OpenAIResponsesAPIConfig.parse_terminal_event_from_stream_chunks(
all_chunks=list(all_chunks)
)
terminal_event: Final = OpenAIResponsesAPIConfig.parse_terminal_event_from_stream_chunks(all_chunks=all_chunks)
if terminal_event is None:
return None
logging_obj.call_type = (
@ -83,7 +82,11 @@ def foreign_azure_deployment(
if match is None:
return None
deployment: Final = match.group(1)
return None if deployment == model_group or deployment in served_models() else deployment
folded: Final = deployment.casefold()
if folded == model_group.casefold():
return None
served: Final = frozenset(name.casefold() for name in served_models())
return None if folded in served else deployment
def without_api_version(api_base: str) -> str:
@ -119,7 +122,9 @@ class AzurePassthroughConfig(BasePassthroughConfig):
relay_base: Final = without_api_version(base_target_url) if caller_api_version else base_target_url
complete_url: Final = BaseAzureLLM._get_base_azure_url(
api_base=relay_base,
litellm_params={**litellm_params, "api_version": caller_api_version or litellm_params.get("api_version")},
litellm_params=MappingProxyType(
{**litellm_params, "api_version": caller_api_version or litellm_params.get("api_version")}
),
route=native_endpoint,
)
return (

View file

@ -620,7 +620,7 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig):
return event_pydantic_model.model_construct(**parsed_chunk)
@staticmethod
def parse_terminal_event_from_stream_chunks(all_chunks: list[str]) -> ResponsesTerminalEvent | None:
def parse_terminal_event_from_stream_chunks(all_chunks: Sequence[str]) -> ResponsesTerminalEvent | None:
for chunk_str in reversed(all_chunks):
for event_model in (ResponseCompletedEvent, ResponseIncompleteEvent, ResponseFailedEvent):
try:

View file

@ -122,6 +122,10 @@ def is_passthrough_request_using_router_model(request_body: dict, llm_router: li
return False
class RelayRejection(TypedDict):
error: ReadOnly[str]
def _deployment_model_name(litellm_params: LiteLLMParamsTypedDict) -> str:
model: Final = litellm_params.get("model", "")
try:
@ -1543,13 +1547,11 @@ async def _relay_azure_router_model(
endpoint, model, lambda: _models_served_by_group(llm_router, model)
)
if foreign_deployment is not None:
raise HTTPException(
status_code=400,
detail={
"error": f"deployment '{foreign_deployment}' in the path is not served by model group '{model}'; "
"put the model group name in the deployments segment"
},
)
rejection: Final[RelayRejection] = {
"error": f"deployment '{foreign_deployment}' in the path is not served by model group '{model}'; "
"put the model group name in the deployments segment"
}
raise HTTPException(status_code=400, detail=rejection)
try:
result: Final = await llm_router.allm_passthrough_route(
model=model,

View file

@ -456,16 +456,26 @@ def test_azure_passthrough_is_streaming_request_reads_the_stream_flag(request_da
("gpt/openai/deployments/gpt/chat/completions", None),
("openai/deployments/gpt/chat/completions", None),
("gpt/openai/deployments/gpt-5.4-mini/chat/completions", None),
("gpt/openai/deployments/GPT-5.4-MINI/chat/completions", None),
("gpt/openai/deployments/Gpt/chat/completions", None),
("gpt/models/chat/completions", None),
("gpt/openai/deployments/gpt-5.4/chat/completions", "gpt-5.4"),
("gpt/openai/deployments/other-group/chat/completions", "other-group"),
("openai/deployments/victim/gpt/chat/completions", "victim"),
("gpt/openai/deployments/GPT-5.4/chat/completions", "GPT-5.4"),
],
)
def test_foreign_azure_deployment_names_a_segment_outside_the_group(endpoint, expected):
assert foreign_azure_deployment(endpoint, "gpt", lambda: frozenset({"gpt-5.4-mini"})) == expected
def test_foreign_azure_deployment_skips_the_router_when_the_segment_is_the_group_itself():
def served_models():
raise AssertionError("the router must not be consulted for the group's own name")
assert foreign_azure_deployment("gpt/openai/deployments/Gpt/chat/completions", "gpt", served_models) is None
@pytest.mark.parametrize(
"endpoint, expected",
[