mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-05 08:07:05 +00:00
chore(providers): add guarded URL model migration path
This commit is contained in:
parent
5ec925b02b
commit
d75f62a17c
11 changed files with 189 additions and 16 deletions
|
|
@ -280,6 +280,9 @@ ssl_security_level: Optional[str] = None
|
|||
ssl_certificate: Optional[str] = None
|
||||
user_url_validation: bool = True
|
||||
user_url_allowed_hosts: List[str] = []
|
||||
reject_url_model_destinations: bool = os.getenv(
|
||||
"LITELLM_REJECT_URL_MODEL_DESTINATIONS", "true"
|
||||
).lower() not in ("false", "0")
|
||||
ssl_ecdh_curve: Optional[str] = (
|
||||
None # Set to 'X25519' to disable PQC and improve performance
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ For vertex ai, check out the vertex_ai/files/handler.py file.
|
|||
|
||||
import time
|
||||
from typing import Any, List, Literal, Optional
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import unquote, urlparse
|
||||
|
||||
import httpx
|
||||
from openai.types.file_deleted import FileDeleted
|
||||
|
|
@ -276,12 +276,20 @@ class GoogleAIStudioFilesHandler(GeminiModelInfo, BaseFilesConfig):
|
|||
@staticmethod
|
||||
def _validate_gemini_file_name(file_name: str) -> None:
|
||||
parts = file_name.split("/")
|
||||
decoded_file_id = ""
|
||||
if len(parts) == 2:
|
||||
decoded_file_id = parts[1]
|
||||
for _ in range(3):
|
||||
next_decoded_file_id = unquote(decoded_file_id)
|
||||
if next_decoded_file_id == decoded_file_id:
|
||||
break
|
||||
decoded_file_id = next_decoded_file_id
|
||||
if (
|
||||
len(parts) != 2
|
||||
or parts[0] != "files"
|
||||
or not parts[1]
|
||||
or parts[1] in {".", ".."}
|
||||
or any(char in parts[1] for char in ("\\", "?", "#"))
|
||||
or decoded_file_id in {".", ".."}
|
||||
or any(char in decoded_file_id for char in ("/", "\\", "?", "#"))
|
||||
):
|
||||
raise ValueError("Invalid Gemini file name")
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from ...openai.chat.gpt_transformation import OpenAIGPTConfig
|
|||
from ..common_utils import (
|
||||
HuggingFaceError,
|
||||
_fetch_inference_provider_mapping,
|
||||
is_url_model_destination,
|
||||
validate_huggingface_model_identifier,
|
||||
)
|
||||
|
||||
|
|
@ -81,7 +82,9 @@ class HuggingFaceChatConfig(OpenAIGPTConfig):
|
|||
Do not add the chat/embedding/rerank extension here. Let the handler do this.
|
||||
"""
|
||||
validate_huggingface_model_identifier(model)
|
||||
if base_url is None:
|
||||
if is_url_model_destination(model):
|
||||
base_url = model
|
||||
elif base_url is None:
|
||||
base_url = os.getenv("HF_API_BASE") or os.getenv("HUGGINGFACE_API_BASE", "")
|
||||
return base_url
|
||||
|
||||
|
|
@ -107,6 +110,9 @@ class HuggingFaceChatConfig(OpenAIGPTConfig):
|
|||
complete_url = str(os.getenv("HF_API_BASE")) or str(
|
||||
os.getenv("HUGGINGFACE_API_BASE")
|
||||
)
|
||||
elif is_url_model_destination(model):
|
||||
complete_url = model
|
||||
complete_url = _build_chat_completion_url(complete_url)
|
||||
# Default construction with provider
|
||||
else:
|
||||
# Parse provider and model
|
||||
|
|
|
|||
|
|
@ -9,16 +9,29 @@ from litellm.llms.base_llm.chat.transformation import BaseLLMException
|
|||
HF_HUB_URL = "https://huggingface.co"
|
||||
|
||||
|
||||
def is_url_model_destination(model: str) -> bool:
|
||||
return model.startswith(("http://", "https://"))
|
||||
|
||||
|
||||
def _should_reject_url_model_destinations() -> bool:
|
||||
import litellm
|
||||
|
||||
return getattr(litellm, "reject_url_model_destinations", True) is True
|
||||
|
||||
|
||||
def validate_huggingface_model_identifier(model: str) -> None:
|
||||
"""Reject URL-valued model identifiers before provider credentials are added."""
|
||||
if "://" not in model:
|
||||
return
|
||||
if is_url_model_destination(model) and not _should_reject_url_model_destinations():
|
||||
return
|
||||
raise HuggingFaceError(
|
||||
status_code=400,
|
||||
message=(
|
||||
"Invalid Hugging Face model identifier. Configure custom endpoints with "
|
||||
"api_base or HF_API_BASE/HUGGINGFACE_API_BASE instead of passing a URL "
|
||||
"as the model."
|
||||
"as the model. To keep legacy URL-valued models for trusted inputs, set "
|
||||
"litellm.reject_url_model_destinations=False."
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,11 @@ from litellm.llms.custom_httpx.http_handler import (
|
|||
from litellm.types.utils import EmbeddingResponse
|
||||
|
||||
from ...base import BaseLLM
|
||||
from ..common_utils import HuggingFaceError, validate_huggingface_model_identifier
|
||||
from ..common_utils import (
|
||||
HuggingFaceError,
|
||||
is_url_model_destination,
|
||||
validate_huggingface_model_identifier,
|
||||
)
|
||||
from .transformation import HuggingFaceEmbeddingConfig
|
||||
|
||||
config = HuggingFaceEmbeddingConfig()
|
||||
|
|
@ -155,6 +159,7 @@ class HuggingFaceEmbedding(BaseLLM):
|
|||
) -> dict:
|
||||
data: Dict = {}
|
||||
validate_huggingface_model_identifier(model)
|
||||
model_uses_url_destination = is_url_model_destination(model)
|
||||
|
||||
## TRANSFORMATION ##
|
||||
if "sentence-transformers" in model:
|
||||
|
|
@ -170,8 +175,12 @@ class HuggingFaceEmbedding(BaseLLM):
|
|||
task_type = optional_params.pop("input_type", None)
|
||||
|
||||
if call_type == "sync":
|
||||
hf_task = get_hf_task_embedding_for_model(
|
||||
model=model, task_type=task_type, api_base=HF_HUB_URL
|
||||
hf_task = (
|
||||
task_type
|
||||
if model_uses_url_destination
|
||||
else get_hf_task_embedding_for_model(
|
||||
model=model, task_type=task_type, api_base=HF_HUB_URL
|
||||
)
|
||||
)
|
||||
elif call_type == "async":
|
||||
return self._async_transform_input(
|
||||
|
|
@ -345,12 +354,19 @@ class HuggingFaceEmbedding(BaseLLM):
|
|||
litellm_params=litellm_params,
|
||||
)
|
||||
task_type = optional_params.get("input_type", None)
|
||||
task = get_hf_task_embedding_for_model(
|
||||
model=model, task_type=task_type, api_base=HF_HUB_URL
|
||||
model_uses_url_destination = is_url_model_destination(model)
|
||||
task = (
|
||||
task_type
|
||||
if model_uses_url_destination
|
||||
else get_hf_task_embedding_for_model(
|
||||
model=model, task_type=task_type, api_base=HF_HUB_URL
|
||||
)
|
||||
)
|
||||
# print_verbose(f"{model}, {task}")
|
||||
embed_url = ""
|
||||
if api_base:
|
||||
if model_uses_url_destination:
|
||||
embed_url = model
|
||||
elif api_base:
|
||||
embed_url = api_base
|
||||
elif "HF_API_BASE" in os.environ:
|
||||
embed_url = os.getenv("HF_API_BASE", "")
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ from ..common_utils import (
|
|||
HuggingFaceError,
|
||||
hf_task_list,
|
||||
hf_tasks,
|
||||
is_url_model_destination,
|
||||
output_parser,
|
||||
validate_huggingface_model_identifier,
|
||||
)
|
||||
|
|
@ -343,7 +344,9 @@ class HuggingFaceEmbeddingConfig(BaseConfig):
|
|||
Do not add the chat/embedding/rerank extension here. Let the handler do this.
|
||||
"""
|
||||
validate_huggingface_model_identifier(model)
|
||||
if api_base is not None:
|
||||
if is_url_model_destination(model):
|
||||
completion_url = model
|
||||
elif api_base is not None:
|
||||
completion_url = api_base
|
||||
elif "HF_API_BASE" in os.environ:
|
||||
completion_url = os.getenv("HF_API_BASE", "")
|
||||
|
|
|
|||
|
|
@ -5,7 +5,11 @@ import litellm
|
|||
from litellm.llms.custom_httpx.http_handler import _get_httpx_client
|
||||
from litellm.utils import EmbeddingResponse, ModelResponse, Usage
|
||||
|
||||
from ..common_utils import OobaboogaError, validate_oobabooga_model_identifier
|
||||
from ..common_utils import (
|
||||
OobaboogaError,
|
||||
is_url_model_destination,
|
||||
validate_oobabooga_model_identifier,
|
||||
)
|
||||
from .transformation import OobaboogaConfig
|
||||
|
||||
oobabooga_config = OobaboogaConfig()
|
||||
|
|
@ -35,7 +39,9 @@ def completion(
|
|||
optional_params=optional_params,
|
||||
litellm_params=litellm_params,
|
||||
)
|
||||
if api_base:
|
||||
if is_url_model_destination(model):
|
||||
completion_url = model
|
||||
elif api_base:
|
||||
completion_url = api_base
|
||||
else:
|
||||
raise OobaboogaError(
|
||||
|
|
@ -96,7 +102,9 @@ def embedding(
|
|||
):
|
||||
# Create completion URL
|
||||
validate_oobabooga_model_identifier(model)
|
||||
if api_base:
|
||||
if is_url_model_destination(model):
|
||||
embeddings_url = model
|
||||
elif api_base:
|
||||
embeddings_url = f"{api_base}/v1/embeddings"
|
||||
else:
|
||||
raise OobaboogaError(
|
||||
|
|
|
|||
|
|
@ -15,14 +15,28 @@ class OobaboogaError(BaseLLMException):
|
|||
super().__init__(status_code=status_code, message=message, headers=headers)
|
||||
|
||||
|
||||
def is_url_model_destination(model: str) -> bool:
|
||||
return model.startswith(("http://", "https://"))
|
||||
|
||||
|
||||
def _should_reject_url_model_destinations() -> bool:
|
||||
import litellm
|
||||
|
||||
return getattr(litellm, "reject_url_model_destinations", True) is True
|
||||
|
||||
|
||||
def validate_oobabooga_model_identifier(model: str) -> None:
|
||||
"""Oobabooga endpoints must be configured with api_base, not model URLs."""
|
||||
if "://" not in model:
|
||||
return
|
||||
if is_url_model_destination(model) and not _should_reject_url_model_destinations():
|
||||
return
|
||||
raise OobaboogaError(
|
||||
status_code=400,
|
||||
message=(
|
||||
"Invalid Oobabooga model identifier. Configure the endpoint with "
|
||||
"api_base instead of passing a URL as the model."
|
||||
"api_base instead of passing a URL as the model. To keep legacy "
|
||||
"URL-valued models for trusted inputs, set "
|
||||
"litellm.reject_url_model_destinations=False."
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -113,6 +113,26 @@ class TestGoogleAIStudioFilesTransformation:
|
|||
litellm_params=litellm_params,
|
||||
)
|
||||
|
||||
def test_transform_retrieve_file_request_rejects_encoded_traversal_name(self):
|
||||
litellm_params = {"api_key": "test-api-key"}
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid Gemini file name"):
|
||||
self.handler.transform_retrieve_file_request(
|
||||
file_id="files/..%2Fsecrets",
|
||||
optional_params={},
|
||||
litellm_params=litellm_params,
|
||||
)
|
||||
|
||||
def test_transform_retrieve_file_request_rejects_double_encoded_separator(self):
|
||||
litellm_params = {"api_key": "test-api-key"}
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid Gemini file name"):
|
||||
self.handler.transform_retrieve_file_request(
|
||||
file_id="files/..%252Fsecrets",
|
||||
optional_params={},
|
||||
litellm_params=litellm_params,
|
||||
)
|
||||
|
||||
@patch.dict("os.environ", {}, clear=True)
|
||||
@patch("litellm.llms.gemini.common_utils.get_secret_str", return_value=None)
|
||||
def test_transform_retrieve_file_request_missing_api_key(self, mock_get_secret):
|
||||
|
|
@ -355,3 +375,16 @@ class TestGoogleAIStudioFilesTransformation:
|
|||
optional_params={},
|
||||
litellm_params=litellm_params,
|
||||
)
|
||||
|
||||
def test_transform_delete_file_request_rejects_encoded_traversal_url(self):
|
||||
litellm_params = {
|
||||
"api_key": "test-api-key",
|
||||
"api_base": "https://generativelanguage.googleapis.com",
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid Gemini file name"):
|
||||
self.handler.transform_delete_file_request(
|
||||
file_id="https://generativelanguage.googleapis.com/v1beta/files/..%2Fsecrets",
|
||||
optional_params={},
|
||||
litellm_params=litellm_params,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from unittest.mock import MagicMock, patch
|
|||
|
||||
import pytest
|
||||
|
||||
import litellm
|
||||
from litellm.llms.huggingface.chat.transformation import HuggingFaceChatConfig
|
||||
from litellm.llms.huggingface.common_utils import HuggingFaceError
|
||||
from litellm.llms.huggingface.embedding.handler import HuggingFaceEmbedding
|
||||
|
|
@ -25,6 +26,23 @@ def test_huggingface_chat_rejects_url_valued_model():
|
|||
assert exc_info.value.status_code == 400
|
||||
|
||||
|
||||
def test_huggingface_chat_allows_legacy_url_model_when_rejection_disabled(
|
||||
monkeypatch,
|
||||
):
|
||||
monkeypatch.setattr(litellm, "reject_url_model_destinations", False)
|
||||
config = HuggingFaceChatConfig()
|
||||
|
||||
complete_url = config.get_complete_url(
|
||||
api_base=None,
|
||||
api_key="hf-secret",
|
||||
model="https://trusted.example",
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
)
|
||||
|
||||
assert complete_url == "https://trusted.example/v1/chat/completions"
|
||||
|
||||
|
||||
def test_huggingface_chat_keeps_explicit_api_base_for_custom_endpoints():
|
||||
config = HuggingFaceChatConfig()
|
||||
|
||||
|
|
@ -51,6 +69,20 @@ def test_huggingface_embedding_config_rejects_url_valued_model():
|
|||
assert exc_info.value.status_code == 400
|
||||
|
||||
|
||||
def test_huggingface_embedding_config_allows_legacy_url_model_when_rejection_disabled(
|
||||
monkeypatch,
|
||||
):
|
||||
monkeypatch.setattr(litellm, "reject_url_model_destinations", False)
|
||||
config = HuggingFaceEmbeddingConfig()
|
||||
|
||||
api_base = config.get_api_base(
|
||||
api_base=None,
|
||||
model="https://trusted.example/embeddings",
|
||||
)
|
||||
|
||||
assert api_base == "https://trusted.example/embeddings"
|
||||
|
||||
|
||||
def test_huggingface_embedding_handler_rejects_before_task_lookup():
|
||||
handler = HuggingFaceEmbedding()
|
||||
logging_obj = MagicMock()
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from unittest.mock import MagicMock, patch
|
|||
|
||||
import pytest
|
||||
|
||||
import litellm
|
||||
from litellm.llms.oobabooga.chat.oobabooga import completion, embedding
|
||||
from litellm.llms.oobabooga.common_utils import OobaboogaError
|
||||
|
||||
|
|
@ -26,6 +27,42 @@ def test_oobabooga_completion_rejects_url_valued_model_before_request():
|
|||
mock_get.assert_not_called()
|
||||
|
||||
|
||||
def test_oobabooga_completion_allows_legacy_url_model_when_rejection_disabled(
|
||||
monkeypatch,
|
||||
):
|
||||
monkeypatch.setattr(litellm, "reject_url_model_destinations", False)
|
||||
response = MagicMock()
|
||||
client = MagicMock()
|
||||
client.post.return_value = response
|
||||
|
||||
with patch(
|
||||
"litellm.llms.oobabooga.chat.oobabooga._get_httpx_client",
|
||||
return_value=client,
|
||||
):
|
||||
with patch(
|
||||
"litellm.llms.oobabooga.chat.oobabooga.oobabooga_config.transform_response",
|
||||
return_value="ok",
|
||||
):
|
||||
result = completion(
|
||||
model="https://trusted.example",
|
||||
messages=[],
|
||||
api_base=None,
|
||||
model_response=MagicMock(),
|
||||
print_verbose=MagicMock(),
|
||||
encoding=MagicMock(),
|
||||
api_key="ooba-secret",
|
||||
logging_obj=MagicMock(),
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
)
|
||||
|
||||
assert result == "ok"
|
||||
client.post.assert_called_once()
|
||||
assert (
|
||||
client.post.call_args.args[0] == "https://trusted.example/v1/chat/completions"
|
||||
)
|
||||
|
||||
|
||||
def test_oobabooga_embedding_rejects_url_valued_model_before_request():
|
||||
with patch(
|
||||
"litellm.llms.oobabooga.chat.oobabooga.litellm.module_level_client.post"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue