mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-02 02:11:58 +00:00
fix(proxy): require OpenAI path segment for shared Azure Cognitive Services domains
Address Greptile review: the `*.cognitiveservices.azure.com` / `*.openai.azure.com` domains are shared by every Azure Cognitive Service (Speech, Vision, Language, ...), so a hostname-only substring match misclassified non-OpenAI Azure traffic as OpenAI routes. - Replace the substring host test with suffix matching (rejects look-alike domains like cognitiveservices.azure.com.attacker.example). - Add `_is_openai_compatible_url` that requires an OpenAI-style path marker (`/openai/` or `/v1/`) on the shared Azure domains, and use it in PassThroughEndpointLogging.is_openai_route (previously hostname-only). - Add negative tests for Azure Speech/Vision paths and look-alike domains. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b7f13957ec
commit
5c915f59a5
3 changed files with 97 additions and 16 deletions
|
|
@ -32,23 +32,70 @@ from litellm.types.passthrough_endpoints.pass_through_endpoints import (
|
|||
from litellm.types.utils import ImageResponse, LlmProviders, PassthroughCallTypes
|
||||
from litellm.utils import ModelResponse, TextCompletionResponse
|
||||
|
||||
# Hostnames that route to OpenAI-compatible APIs. `cognitiveservices.azure.com`
|
||||
# is the Azure subdomain used by newer Azure OpenAI resource types (e.g. the
|
||||
# "Azure AI Foundry" / Cognitive Services-hosted deployments). Older Azure
|
||||
# OpenAI resources use `openai.azure.com`; both are valid in production.
|
||||
_OPENAI_COMPATIBLE_HOSTNAMES = (
|
||||
"api.openai.com",
|
||||
"openai.azure.com",
|
||||
"cognitiveservices.azure.com",
|
||||
)
|
||||
# Hostnames that route to OpenAI-compatible APIs.
|
||||
#
|
||||
# `api.openai.com` is OpenAI proper. The two Azure domains below are *shared by
|
||||
# every Azure Cognitive Service* (Speech, Vision, Language, ...), not just Azure
|
||||
# OpenAI: `openai.azure.com` is the classic Azure OpenAI domain, while
|
||||
# `cognitiveservices.azure.com` is used by newer "Azure AI Foundry" /
|
||||
# Cognitive Services-hosted Azure OpenAI deployments. Because the hostname alone
|
||||
# cannot tell Azure OpenAI apart from the other Cognitive Services on those
|
||||
# domains, requests there must additionally carry an OpenAI-style path segment.
|
||||
_OPENAI_HOSTNAMES = ("api.openai.com",)
|
||||
_AZURE_OPENAI_HOSTNAMES = ("openai.azure.com", "cognitiveservices.azure.com")
|
||||
# Path markers that identify an Azure request as Azure OpenAI rather than Speech
|
||||
# / Vision / Language / ... `/openai/` is the native Azure OpenAI path prefix;
|
||||
# `/v1/` is the OpenAI-v1 surface used by LiteLLM's pass-through routing. Other
|
||||
# Cognitive Services use service-named prefixes and versions like `/v3.1/`,
|
||||
# `/v1.0/`, so they do not collide with these markers.
|
||||
_AZURE_OPENAI_PATH_MARKERS = ("/openai/", "/v1/")
|
||||
|
||||
|
||||
def _hostname_matches(hostname: str, suffixes: tuple) -> bool:
|
||||
"""True if hostname equals one of `suffixes` or is a subdomain of it.
|
||||
|
||||
Uses suffix matching (not a bare substring test) so look-alikes such as
|
||||
`cognitiveservices.azure.com.attacker.example` are not accepted.
|
||||
"""
|
||||
return any(
|
||||
hostname == suffix or hostname.endswith("." + suffix) for suffix in suffixes
|
||||
)
|
||||
|
||||
|
||||
def _is_openai_compatible_host(hostname: Optional[str]) -> bool:
|
||||
"""True if the hostname is one of the recognized OpenAI-compatible
|
||||
surfaces (OpenAI proper or any Azure OpenAI subdomain)."""
|
||||
"""True if the hostname is OpenAI proper or one of the Azure OpenAI domains.
|
||||
|
||||
Hostname-only check, kept for the route-level helpers that additionally
|
||||
require a specific OpenAI path (e.g. `/v1/chat/completions`). When only the
|
||||
hostname would otherwise gate dispatch, use `_is_openai_compatible_url` so
|
||||
non-OpenAI Azure Cognitive Services on the shared domains are excluded.
|
||||
"""
|
||||
if not hostname:
|
||||
return False
|
||||
return any(host in hostname for host in _OPENAI_COMPATIBLE_HOSTNAMES)
|
||||
return _hostname_matches(hostname, _OPENAI_HOSTNAMES) or _hostname_matches(
|
||||
hostname, _AZURE_OPENAI_HOSTNAMES
|
||||
)
|
||||
|
||||
|
||||
def _is_openai_compatible_url(url_route: Optional[str]) -> bool:
|
||||
"""True if the URL targets an OpenAI-compatible API surface.
|
||||
|
||||
For the shared Azure Cognitive Services domains we additionally require an
|
||||
OpenAI-style path segment (`/openai/` or `/v1/`) so non-OpenAI Azure services
|
||||
(Speech, Vision, Language, ...) on the same domain are not misclassified as
|
||||
OpenAI routes.
|
||||
"""
|
||||
if not url_route:
|
||||
return False
|
||||
parsed_url = urlparse(url_route)
|
||||
hostname = parsed_url.hostname
|
||||
if not hostname:
|
||||
return False
|
||||
if _hostname_matches(hostname, _OPENAI_HOSTNAMES):
|
||||
return True
|
||||
if _hostname_matches(hostname, _AZURE_OPENAI_HOSTNAMES):
|
||||
return any(marker in parsed_url.path for marker in _AZURE_OPENAI_PATH_MARKERS)
|
||||
return False
|
||||
|
||||
|
||||
class OpenAIPassthroughLoggingHandler(BasePassthroughLoggingHandler):
|
||||
|
|
|
|||
|
|
@ -434,15 +434,19 @@ class PassThroughEndpointLogging:
|
|||
return False
|
||||
|
||||
def is_openai_route(self, url_route: str):
|
||||
"""Check if the URL route is an OpenAI API route."""
|
||||
"""Check if the URL route is an OpenAI API route.
|
||||
|
||||
Uses the URL-aware helper so that non-OpenAI Azure Cognitive Services
|
||||
(Speech, Vision, Language, ...) sharing the `*.cognitiveservices.azure.com`
|
||||
/ `*.openai.azure.com` domains are not misclassified as OpenAI routes.
|
||||
"""
|
||||
if not url_route:
|
||||
return False
|
||||
from .llm_provider_handlers.openai_passthrough_logging_handler import (
|
||||
_is_openai_compatible_host,
|
||||
_is_openai_compatible_url,
|
||||
)
|
||||
|
||||
parsed_url = urlparse(url_route)
|
||||
return _is_openai_compatible_host(parsed_url.hostname)
|
||||
return _is_openai_compatible_url(url_route)
|
||||
|
||||
def is_gemini_route(
|
||||
self, url_route: str, custom_llm_provider: Optional[str] = None
|
||||
|
|
|
|||
|
|
@ -824,6 +824,14 @@ class TestOpenAIPassthroughIntegration:
|
|||
== True
|
||||
)
|
||||
assert self.handler.is_openai_route("https://api.openai.com/v1/models") == True
|
||||
# Azure OpenAI on the shared Cognitive Services domain, identified by an
|
||||
# OpenAI-style path segment.
|
||||
assert (
|
||||
self.handler.is_openai_route(
|
||||
"https://my-resource.cognitiveservices.azure.com/v1/chat/completions"
|
||||
)
|
||||
== True
|
||||
)
|
||||
|
||||
# Negative cases
|
||||
assert (
|
||||
|
|
@ -840,6 +848,28 @@ class TestOpenAIPassthroughIntegration:
|
|||
self.handler.is_openai_route("https://api.assemblyai.com/v2/transcript")
|
||||
== False
|
||||
)
|
||||
# Non-OpenAI Azure Cognitive Services share the `cognitiveservices.azure.com`
|
||||
# domain but must NOT be classified as OpenAI routes (no OpenAI path segment).
|
||||
assert (
|
||||
self.handler.is_openai_route(
|
||||
"https://my-resource.cognitiveservices.azure.com/speechtotext/v3.1/recognize"
|
||||
)
|
||||
== False
|
||||
)
|
||||
assert (
|
||||
self.handler.is_openai_route(
|
||||
"https://my-resource.cognitiveservices.azure.com/vision/v3.2/analyze"
|
||||
)
|
||||
== False
|
||||
)
|
||||
# A look-alike domain that merely contains an OpenAI host as a substring
|
||||
# must be rejected by the suffix-based hostname match.
|
||||
assert (
|
||||
self.handler.is_openai_route(
|
||||
"https://cognitiveservices.azure.com.attacker.example/v1/chat/completions"
|
||||
)
|
||||
== False
|
||||
)
|
||||
assert self.handler.is_openai_route("") == False
|
||||
|
||||
@patch(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue