lint url encoding
Some checks failed
Unit Tests: Caching (Redis) / caching-redis (push) Has been cancelled
Unit Tests: Proxy DB Operations / assert-shard-coverage (push) Has been cancelled
Unit Tests: Security / security (push) Has been cancelled
Unit Tests: Proxy DB Operations / proxy-utils (push) Has been cancelled
Unit Tests: Proxy DB Operations / auth-checks (push) Has been cancelled
Unit Tests: Proxy DB Operations / budgets (push) Has been cancelled
Unit Tests: Proxy DB Operations / custom-logging (push) Has been cancelled
Unit Tests: Proxy DB Operations / db-and-spend (push) Has been cancelled
Unit Tests: Proxy DB Operations / endpoints-and-responses (push) Has been cancelled
Unit Tests: Proxy DB Operations / guardrails-hooks (push) Has been cancelled
Unit Tests: Proxy DB Operations / jwt-and-keys (push) Has been cancelled
Unit Tests: Proxy DB Operations / key-generation (push) Has been cancelled
Unit Tests: Proxy DB Operations / logging-misc (push) Has been cancelled
Unit Tests: Proxy DB Operations / proxy-runtime (push) Has been cancelled
Unit Tests: Proxy DB Operations / proxy-server-core (push) Has been cancelled
Unit Tests: Proxy DB Operations / schema-migration (push) Has been cancelled

This commit is contained in:
Michael Riad Zaky 2026-04-22 17:01:28 -07:00 • committed by Michael Riad Zaky
parent c05de83f1c
commit aa294dfb4b
62 changed files with 967 additions and 139 deletions

View file

@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Union, cas
import httpx
from httpx import Headers, Response
from litellm.llms.base_llm._url_utils import encode_path_segment
from litellm.llms.base_llm.batches.transformation import BaseBatchesConfig
from litellm.llms.base_llm.chat.transformation import BaseLLMException
from litellm.types.llms.openai import AllMessageValues, CreateBatchRequest
@ -122,7 +123,7 @@ class AnthropicBatchesConfig(BaseBatchesConfig):
Complete URL for Anthropic batch retrieval: {api_base}/v1/messages/batches/{batch_id}
"""
api_base = api_base or self.anthropic_model_info.get_api_base(api_base)
return f"{api_base.rstrip('/')}/v1/messages/batches/{batch_id}"
return f"{api_base.rstrip('/')}/v1/messages/batches/{encode_path_segment(batch_id)}"
def transform_retrieve_batch_request(
self,

View file

@ -9,6 +9,7 @@ import litellm
from litellm._logging import verbose_logger
from litellm._uuid import uuid
from litellm.litellm_core_utils.litellm_logging import Logging
from litellm.llms.base_llm._url_utils import encode_path_segment
from litellm.llms.custom_httpx.http_handler import get_async_httpx_client
from litellm.types.llms.openai import (
FileContentRequest,
@ -89,7 +90,7 @@ class AnthropicFilesHandler:
raise ValueError("Missing Anthropic API Key")
# Construct the Anthropic batch results URL
results_url = f"{api_base.rstrip('/')}/v1/messages/batches/{batch_id}/results"
results_url = f"{api_base.rstrip('/')}/v1/messages/batches/{encode_path_segment(batch_id)}/results"
# Prepare headers
headers = {

View file

@ -20,6 +20,7 @@ import httpx
from openai.types.file_deleted import FileDeleted
from litellm.litellm_core_utils.prompt_templates.common_utils import extract_file_data
from litellm.llms.base_llm._url_utils import encode_path_segment
from litellm.llms.base_llm.chat.transformation import BaseLLMException
from litellm.llms.base_llm.files.transformation import (
BaseFilesConfig,
@ -185,7 +186,7 @@ class AnthropicFilesConfig(BaseFilesConfig):
AnthropicModelInfo.get_api_base(litellm_params.get("api_base"))
or ANTHROPIC_FILES_API_BASE
)
return f"{api_base.rstrip('/')}/v1/files/{file_id}", {}
return f"{api_base.rstrip('/')}/v1/files/{encode_path_segment(file_id)}", {}
def transform_retrieve_file_response(
self,
@ -206,7 +207,7 @@ class AnthropicFilesConfig(BaseFilesConfig):
AnthropicModelInfo.get_api_base(litellm_params.get("api_base"))
or ANTHROPIC_FILES_API_BASE
)
return f"{api_base.rstrip('/')}/v1/files/{file_id}", {}
return f"{api_base.rstrip('/')}/v1/files/{encode_path_segment(file_id)}", {}
def transform_delete_file_response(
self,
@ -268,7 +269,10 @@ class AnthropicFilesConfig(BaseFilesConfig):
AnthropicModelInfo.get_api_base(litellm_params.get("api_base"))
or ANTHROPIC_FILES_API_BASE
)
return f"{api_base.rstrip('/')}/v1/files/{file_id}/content", {}
return (
f"{api_base.rstrip('/')}/v1/files/{encode_path_segment(file_id)}/content",
{},
)
def transform_file_content_response(
self,

View file

@ -7,6 +7,7 @@ from typing import Any, Dict, Optional, Tuple
import httpx
from litellm._logging import verbose_logger
from litellm.llms.base_llm._url_utils import encode_path_segment
from litellm.llms.base_llm.skills.transformation import (
BaseSkillsAPIConfig,
LiteLLMLoggingObj,
@ -81,7 +82,7 @@ class AnthropicSkillsConfig(BaseSkillsAPIConfig):
api_base = AnthropicModelInfo.get_api_base()
if skill_id:
return f"{api_base}/v1/skills/{skill_id}"
return f"{api_base}/v1/skills/{encode_path_segment(skill_id)}"
return f"{api_base}/v1/{endpoint}"
def transform_create_skill_request(

View file

@ -8,6 +8,7 @@ from openai import AsyncAzureOpenAI, AsyncOpenAI, AzureOpenAI, OpenAI
import litellm
from litellm._logging import verbose_logger
from litellm.caching.caching import DualCache
from litellm.llms.base_llm._url_utils import encode_path_segment
from litellm.llms.base_llm.chat.transformation import BaseLLMException
from litellm.llms.openai.common_utils import BaseOpenAILLM
from litellm.secret_managers.get_azure_ad_token_provider import (
@ -211,7 +212,7 @@ def get_azure_ad_token_from_oidc(
client = litellm.module_level_client
req_token = client.post(
f"{azure_authority_host}/{azure_tenant_id}/oauth2/v2.0/token",
f"{azure_authority_host}/{encode_path_segment(azure_tenant_id)}/oauth2/v2.0/token",
data={
"client_id": azure_client_id,
"grant_type": "client_credentials",

View file

@ -6,6 +6,7 @@ from openai.types.responses import ResponseReasoningItem
from litellm._logging import verbose_logger
from litellm.llms.azure.common_utils import BaseAzureLLM
from litellm.llms.base_llm._url_utils import encode_path_segment
from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig
from litellm.types.llms.openai import *
from litellm.types.responses.main import *
@ -201,7 +202,7 @@ class AzureOpenAIResponsesAPIConfig(OpenAIResponsesAPIConfig):
# Insert the response_id at the end of the path component
# Remove trailing slash if present to avoid double slashes
path = parsed_url.path.rstrip("/")
new_path = f"{path}/{response_id}"
new_path = f"{path}/{encode_path_segment(response_id)}"
# Reconstruct the URL with all original components but with the modified path
constructed_url = urlunparse(
@ -322,7 +323,7 @@ class AzureOpenAIResponsesAPIConfig(OpenAIResponsesAPIConfig):
# Insert the response_id and /cancel at the end of the path component
# Remove trailing slash if present to avoid double slashes
path = parsed_url.path.rstrip("/")
new_path = f"{path}/{response_id}/cancel"
new_path = f"{path}/{encode_path_segment(response_id)}/cancel"
# Reconstruct the URL with all original components but with the modified path
cancel_url = urlunparse(

View file

@ -40,6 +40,7 @@ from litellm.llms.azure_ai.agents.transformation import (
AzureAIAgentsConfig,
AzureAIAgentsError,
)
from litellm.llms.base_llm._url_utils import encode_path_segment
from litellm.types.utils import ModelResponse
if TYPE_CHECKING:
@ -75,20 +76,20 @@ class AzureAIAgentsHandler:
def _build_messages_url(
self, api_base: str, thread_id: str, api_version: str
) -> str:
return f"{api_base}/threads/{thread_id}/messages?api-version={api_version}"
return f"{api_base}/threads/{encode_path_segment(thread_id)}/messages?api-version={api_version}"
def _build_runs_url(self, api_base: str, thread_id: str, api_version: str) -> str:
return f"{api_base}/threads/{thread_id}/runs?api-version={api_version}"
return f"{api_base}/threads/{encode_path_segment(thread_id)}/runs?api-version={api_version}"
def _build_run_status_url(
self, api_base: str, thread_id: str, run_id: str, api_version: str
) -> str:
return f"{api_base}/threads/{thread_id}/runs/{run_id}?api-version={api_version}"
return f"{api_base}/threads/{encode_path_segment(thread_id)}/runs/{encode_path_segment(run_id)}?api-version={api_version}"
def _build_list_messages_url(
self, api_base: str, thread_id: str, api_version: str
) -> str:
return f"{api_base}/threads/{thread_id}/messages?api-version={api_version}"
return f"{api_base}/threads/{encode_path_segment(thread_id)}/messages?api-version={api_version}"
def _build_create_thread_and_run_url(self, api_base: str, api_version: str) -> str:
"""URL for the create-thread-and-run endpoint (supports streaming)."""

View file

@ -22,6 +22,7 @@ from litellm.constants import (
AZURE_DOCUMENT_INTELLIGENCE_DEFAULT_DPI,
AZURE_OPERATION_POLLING_TIMEOUT,
)
from litellm.llms.base_llm._url_utils import encode_path_segment
from litellm.llms.base_llm.ocr.transformation import (
BaseOCRConfig,
DocumentType,
@ -220,10 +221,7 @@ class AzureDocumentIntelligenceOCRConfig(BaseOCRConfig):
# Azure Document Intelligence analyze endpoint
# Note: API version 2024-11-30+ uses /documentintelligence/ (not /formrecognizer/)
url = (
f"{api_base}/documentintelligence/documentModels/{model_id}:analyze"
f"?api-version={AZURE_DOCUMENT_INTELLIGENCE_API_VERSION}"
)
url = f"{api_base}/documentintelligence/documentModels/{encode_path_segment(model_id)}:analyze?api-version={AZURE_DOCUMENT_INTELLIGENCE_API_VERSION}"
# Azure DI accepts `pages` as a query param (1-based, e.g. "1-3,5").
# `optional_params` has already been normalized in `map_ocr_params`.

View file

@ -4,6 +4,7 @@ import httpx
import litellm
from litellm.llms.azure.common_utils import BaseAzureLLM
from litellm.llms.base_llm._url_utils import encode_path_segment
from litellm.llms.base_llm.vector_store.transformation import BaseVectorStoreConfig
from litellm.types.router import GenericLiteLLMParams
from litellm.types.vector_stores import (
@ -139,7 +140,7 @@ class AzureAIVectorStoreConfig(BaseVectorStoreConfig, BaseAzureLLM):
# Azure AI Search endpoint for search
index_name = vector_store_id # vector_store_id is the index name
url = f"{api_base}/indexes/{index_name}/docs/search?api-version=2024-07-01"
url = f"{api_base}/indexes/{encode_path_segment(index_name)}/docs/search?api-version=2024-07-01"
# Build the request body for Azure AI Search with vector search
request_body = {

View file

@ -0,0 +1,37 @@
"""URL-encoding helpers for provider transformations."""
from typing import Optional
from urllib.parse import quote
def _reject_dot_segment(segment: str, full: str) -> None:
if segment in ("..", "."):
raise ValueError(f"Illegal path segment in identifier: {full!r}")
def encode_path_segment(segment: Optional[str], safe: str = "") -> str:
"""Percent-encode a single path segment. Raises on empty / ``None`` / ``.`` / ``..``.
``safe`` is forwarded to ``urllib.parse.quote`` for callers that need to
preserve specific characters (e.g. ``:`` in Bedrock model IDs).
"""
if segment is None or segment == "":
raise ValueError("identifier is required, got empty or None")
str_segment = str(segment)
_reject_dot_segment(str_segment, str_segment)
return quote(str_segment, safe=safe)
def encode_url_path(path: Optional[str]) -> str:
"""Percent-encode a multi-segment path; preserves ``/`` and ``@``.
``None`` / ``""`` → ``""``. Rejects ``.``, ``..``, or empty segments.
"""
if path is None or path == "":
return ""
str_path = str(path)
for segment in str_path.split("/"):
if segment == "":
raise ValueError(f"Empty path segment in identifier: {str_path!r}")
_reject_dot_segment(segment, str_path)
return quote(str_path, safe="/@")

View file

@ -15,6 +15,7 @@ from litellm._uuid import uuid
from litellm.litellm_core_utils.prompt_templates.common_utils import (
convert_content_list_to_str,
)
from litellm.llms.base_llm._url_utils import encode_path_segment
from litellm.llms.base_llm.chat.transformation import BaseConfig, BaseLLMException
from litellm.llms.bedrock.base_aws_llm import BaseAWSLLM
from litellm.llms.bedrock.common_utils import BedrockError
@ -98,7 +99,7 @@ class AmazonInvokeAgentConfig(BaseConfig, BaseAWSLLM):
agent_id, agent_alias_id = self._get_agent_id_and_alias_id(model)
session_id = self._get_session_id(optional_params)
endpoint_url = f"{endpoint_url}/agents/{agent_id}/agentAliases/{agent_alias_id}/sessions/{session_id}/text"
endpoint_url = f"{endpoint_url}/agents/{encode_path_segment(agent_id)}/agentAliases/{encode_path_segment(agent_alias_id)}/sessions/{encode_path_segment(session_id)}/text"
return endpoint_url

View file

@ -11,6 +11,7 @@ from typing import TYPE_CHECKING, Any, List, Optional, Tuple, Union
import httpx
from litellm.llms.base_llm._url_utils import encode_path_segment
from litellm.llms.bedrock.base_aws_llm import BaseAWSLLM
from litellm.llms.bedrock.common_utils import BedrockError
from litellm.llms.openai.chat.gpt_transformation import OpenAIGPTConfig
@ -98,6 +99,9 @@ class AmazonBedrockOpenAIConfig(OpenAIGPTConfig, BaseAWSLLM):
# Encode model ID for ARNs (e.g., :imported-model/ -> :imported-model%2F)
model_id = CommonUtils.encode_bedrock_runtime_modelid_arn(model_id)
if "arn:aws:" not in model_id:
# Bedrock model IDs use ':' for version (e.g. amazon.nova-pro-v1:0)
model_id = encode_path_segment(model_id, safe=":")
# Build the invoke URL
if stream:

View file

@ -8,6 +8,7 @@ to AWS Bedrock's CountTokens API format and vice versa.
import re
from typing import Any, Dict, List, Optional
from litellm.llms.base_llm._url_utils import encode_path_segment
from litellm.llms.bedrock.base_aws_llm import BaseAWSLLM
from litellm.llms.bedrock.common_utils import get_bedrock_base_model
@ -207,7 +208,10 @@ class BedrockCountTokensConfig(BaseAWSLLM):
aws_bedrock_runtime_endpoint=aws_bedrock_runtime_endpoint,
aws_region_name=aws_region_name,
)
endpoint = f"{base_url}/model/{model_id}/count-tokens"
# Bedrock model IDs use ':' for version (e.g. amazon.nova-pro-v1:0)
endpoint = (
f"{base_url}/model/{encode_path_segment(model_id, safe=':')}/count-tokens"
)
return endpoint

View file

@ -11,6 +11,7 @@ from litellm._logging import verbose_logger
from litellm._uuid import uuid
from litellm.files.utils import FilesAPIUtils
from litellm.litellm_core_utils.prompt_templates.common_utils import extract_file_data
from litellm.llms.base_llm._url_utils import encode_path_segment, encode_url_path
from litellm.llms.base_llm.chat.transformation import BaseLLMException
from litellm.llms.base_llm.files.transformation import (
BaseFilesConfig,
@ -195,7 +196,7 @@ class BedrockFilesConfig(BaseAWSLLM, BaseFilesConfig):
or f"https://s3.{aws_region_name}.amazonaws.com"
)
return f"{s3_endpoint_url}/{bucket_name}/{object_name}"
return f"{s3_endpoint_url}/{encode_path_segment(bucket_name)}/{encode_url_path(object_name)}"
def get_supported_openai_params(
self, model: str

View file

@ -3,6 +3,7 @@ from urllib.parse import urlparse
import httpx
from litellm.llms.base_llm._url_utils import encode_path_segment
from litellm.llms.base_llm.vector_store.transformation import BaseVectorStoreConfig
from litellm.llms.bedrock.base_aws_llm import BaseAWSLLM
from litellm.types.integrations.rag.bedrock_knowledgebase import (
@ -206,7 +207,7 @@ class BedrockVectorStoreConfig(BaseVectorStoreConfig, BaseAWSLLM):
if isinstance(query, list):
query = " ".join(query)
url = f"{api_base}/{vector_store_id}/retrieve"
url = f"{api_base}/{encode_path_segment(vector_store_id)}/retrieve"
request_body: Dict[str, Any] = {
"retrievalQuery": BedrockKBRetrievalQuery(text=query),

View file

@ -7,6 +7,7 @@ import httpx
from litellm.litellm_core_utils.exception_mapping_utils import exception_type
from litellm.litellm_core_utils.logging_utils import track_llm_api_timing
from litellm.llms.base_llm._url_utils import encode_url_path
from litellm.llms.base_llm.chat.transformation import BaseConfig, BaseLLMException
from litellm.llms.custom_httpx.http_handler import (
AsyncHTTPHandler,
@ -149,7 +150,7 @@ class BytezChatConfig(BaseConfig):
litellm_params: dict,
stream: Optional[bool] = None,
) -> str:
return f"{API_BASE}/{model}"
return f"{API_BASE}/{encode_url_path(model)}"
def transform_request(
self,

View file

@ -5,6 +5,7 @@ from typing import AsyncIterator, Iterator, List, Optional, Union
import httpx
import litellm
from litellm.llms.base_llm._url_utils import encode_url_path
from litellm.llms.base_llm.base_model_iterator import BaseModelResponseIterator
from litellm.llms.base_llm.chat.transformation import (
BaseConfig,
@ -89,7 +90,7 @@ class CloudflareChatConfig(BaseConfig):
api_base = (
f"https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/run/"
)
return api_base + model
return api_base + encode_url_path(model)
def get_supported_openai_params(self, model: str) -> List[str]:
return [

View file

@ -12,6 +12,7 @@ from typing import TYPE_CHECKING, Any, Coroutine, Dict, Optional, Type, Union
import httpx
import litellm
from litellm.llms.base_llm._url_utils import encode_path_segment
from litellm.llms.custom_httpx.http_handler import (
AsyncHTTPHandler,
HTTPHandler,
@ -72,7 +73,9 @@ def _build_url(
# Substitute path parameters
for param, value in path_params.items():
path_template = path_template.replace(f"{{{param}}}", value)
path_template = path_template.replace(
f"{{{param}}}", encode_path_segment(value)
)
# Parse the api_base to extract existing query params
parsed_base = httpx.URL(api_base)

View file

@ -26,6 +26,7 @@ from litellm._logging import _redact_string, verbose_logger
from litellm.anthropic_beta_headers_manager import update_headers_with_filtered_beta
from litellm.constants import REALTIME_WEBSOCKET_MAX_MESSAGE_SIZE_BYTES
from litellm.litellm_core_utils.realtime_streaming import RealTimeStreaming
from litellm.llms.base_llm._url_utils import encode_path_segment
from litellm.llms.base_llm.anthropic_messages.transformation import (
BaseAnthropicMessagesConfig,
)
@ -8907,7 +8908,7 @@ class BaseLLMHTTPHandler:
litellm_params=dict(litellm_params),
)
url = f"{api_base}/{vector_store_id}"
url = f"{api_base}/{encode_path_segment(vector_store_id)}"
logging_obj.pre_call(
input="",
@ -8974,7 +8975,7 @@ class BaseLLMHTTPHandler:
litellm_params=dict(litellm_params),
)
url = f"{api_base}/{vector_store_id}"
url = f"{api_base}/{encode_path_segment(vector_store_id)}"
logging_obj.pre_call(
input="",
@ -9173,7 +9174,7 @@ class BaseLLMHTTPHandler:
litellm_params=dict(litellm_params),
)
url = f"{api_base}/{vector_store_id}"
url = f"{api_base}/{encode_path_segment(vector_store_id)}"
request_body: Dict[str, Any] = dict(vector_store_update_optional_params)
@ -9256,7 +9257,7 @@ class BaseLLMHTTPHandler:
litellm_params=dict(litellm_params),
)
url = f"{api_base}/{vector_store_id}"
url = f"{api_base}/{encode_path_segment(vector_store_id)}"
request_body: Dict[str, Any] = dict(vector_store_update_optional_params)
@ -9322,7 +9323,7 @@ class BaseLLMHTTPHandler:
litellm_params=dict(litellm_params),
)
url = f"{api_base}/{vector_store_id}"
url = f"{api_base}/{encode_path_segment(vector_store_id)}"
logging_obj.pre_call(
input="",
@ -9387,7 +9388,7 @@ class BaseLLMHTTPHandler:
litellm_params=dict(litellm_params),
)
url = f"{api_base}/{vector_store_id}"
url = f"{api_base}/{encode_path_segment(vector_store_id)}"
logging_obj.pre_call(
input="",

View file

@ -12,6 +12,7 @@ from httpx import Headers
import litellm
from litellm.types.utils import all_litellm_params
from litellm.llms.base_llm._url_utils import encode_path_segment
from litellm.llms.base_llm.chat.transformation import BaseLLMException
from litellm.llms.base_llm.text_to_speech.transformation import (
BaseTextToSpeechConfig,
@ -321,7 +322,7 @@ class ElevenLabsTextToSpeechConfig(BaseTextToSpeechConfig):
"ElevenLabs voice_id is required. Pass `voice` when calling `litellm.speech()`."
)
url = f"{base_url}{self.TTS_ENDPOINT_PATH}/{voice_id}"
url = f"{base_url}{self.TTS_ENDPOINT_PATH}/{encode_path_segment(voice_id)}"
query_params = litellm_params.get(self.ELEVENLABS_QUERY_PARAMS_KEY, {})
if query_params:

View file

@ -10,6 +10,7 @@ from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLogging
from litellm.litellm_core_utils.llm_response_utils.get_headers import (
get_response_headers,
)
from litellm.llms.base_llm._url_utils import encode_path_segment
from litellm.secret_managers.main import get_secret_str
from litellm.types.llms.openai import (
AllMessageValues,
@ -438,7 +439,7 @@ class FireworksAIConfig(OpenAIGPTConfig):
if base.endswith("/v1"):
base = base[: -len("/v1")]
response = litellm.module_level_client.get(
url=f"{base}/v1/accounts/{account_id}/models",
url=f"{base}/v1/accounts/{encode_path_segment(account_id)}/models",
headers={"Authorization": f"Bearer {api_key}"},
)

View file

@ -3,6 +3,7 @@ from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Union
import httpx
import litellm
from litellm.llms.base_llm._url_utils import encode_url_path
from litellm.llms.custom_httpx.http_handler import get_async_httpx_client
from litellm.types.utils import LlmProviders
@ -54,7 +55,7 @@ class GoogleAIStudioTokenCounter:
Construct the URL for the Google Gen AI Studio countTokens endpoint.
"""
base_url = api_base or "https://generativelanguage.googleapis.com"
return f"{base_url}/v1beta/models/{model}:countTokens"
return f"{base_url}/v1beta/models/{encode_url_path(model)}:countTokens"
async def validate_environment(
self,

View file

@ -13,6 +13,7 @@ from openai.types.file_deleted import FileDeleted
from litellm._logging import verbose_logger
from litellm.litellm_core_utils.prompt_templates.common_utils import extract_file_data
from litellm.llms.base_llm._url_utils import encode_url_path
from litellm.llms.base_llm.files.transformation import (
BaseFilesConfig,
LiteLLMLoggingObj,
@ -232,7 +233,7 @@ class GoogleAIStudioFilesHandler(GeminiModelInfo, BaseFilesConfig):
)
api_base = api_base.rstrip("/")
url = f"{api_base}/v1beta/{file_part}"
url = f"{api_base}/v1beta/{encode_url_path(file_part)}"
# API key is passed via x-goog-api-key header (set in validate_environment)
return url, {}
@ -346,7 +347,7 @@ class GoogleAIStudioFilesHandler(GeminiModelInfo, BaseFilesConfig):
file_name = file_id if file_id.startswith("files/") else f"files/{file_id}"
# Construct the delete URL
url = f"{api_base}/v1beta/{file_name}"
url = f"{api_base}/v1beta/{encode_url_path(file_name)}"
# Add API key as header (Google AI Studio uses x-goog-api-key header)
params: dict = {}

View file

@ -6,6 +6,7 @@ import httpx
from httpx._types import RequestFiles
from litellm.images.utils import ImageEditRequestUtils
from litellm.llms.base_llm._url_utils import encode_url_path
from litellm.llms.base_llm.image_edit.transformation import BaseImageEditConfig
from litellm.secret_managers.main import get_secret_str
from litellm.types.images.main import ImageEditOptionalRequestParams
@ -79,7 +80,7 @@ class GeminiImageEditConfig(BaseImageEditConfig):
api_base or get_secret_str("GEMINI_API_BASE") or self.DEFAULT_BASE_URL
)
base_url = base_url.rstrip("/")
return f"{base_url}/models/{model}:generateContent"
return f"{base_url}/models/{encode_url_path(model)}:generateContent"
def transform_image_edit_request( # type: ignore[override]
self,

View file

@ -2,6 +2,7 @@ from typing import TYPE_CHECKING, Any, List, Optional
import httpx
from litellm.llms.base_llm._url_utils import encode_url_path
from litellm.llms.base_llm.image_generation.transformation import (
BaseImageGenerationConfig,
)
@ -127,11 +128,12 @@ class GoogleImageGenConfig(BaseImageGenerationConfig):
complete_url = complete_url.rstrip("/")
# Gemini Flash Image Preview models use generateContent endpoint
encoded_model = encode_url_path(model)
if "gemini" in model:
complete_url = f"{complete_url}/models/{model}:generateContent"
complete_url = f"{complete_url}/models/{encoded_model}:generateContent"
else:
# All other Imagen models use predict endpoint
complete_url = f"{complete_url}/models/{model}:predict"
complete_url = f"{complete_url}/models/{encoded_model}:predict"
return complete_url

View file

@ -15,6 +15,7 @@ import httpx
from litellm._logging import verbose_logger
from litellm.litellm_core_utils.core_helpers import process_response_headers
from litellm.llms.base_llm._url_utils import encode_path_segment
from litellm.llms.base_llm.interactions.transformation import BaseInteractionsAPIConfig
from litellm.llms.gemini.common_utils import GeminiError, GeminiModelInfo
from litellm.types.interactions import (
@ -206,7 +207,7 @@ class GoogleAIStudioInteractionsConfig(BaseInteractionsAPIConfig):
if not GeminiModelInfo.get_api_key(litellm_params.api_key):
raise ValueError("Google API key is required")
return (
f"{resolved_api_base}/{self.api_version}/interactions/{interaction_id}",
f"{resolved_api_base}/{self.api_version}/interactions/{encode_path_segment(interaction_id)}",
{},
)
@ -239,7 +240,7 @@ class GoogleAIStudioInteractionsConfig(BaseInteractionsAPIConfig):
if not GeminiModelInfo.get_api_key(litellm_params.api_key):
raise ValueError("Google API key is required")
return (
f"{resolved_api_base}/{self.api_version}/interactions/{interaction_id}",
f"{resolved_api_base}/{self.api_version}/interactions/{encode_path_segment(interaction_id)}",
{},
)
@ -269,7 +270,7 @@ class GoogleAIStudioInteractionsConfig(BaseInteractionsAPIConfig):
if not GeminiModelInfo.get_api_key(litellm_params.api_key):
raise ValueError("Google API key is required")
return (
f"{resolved_api_base}/{self.api_version}/interactions/{interaction_id}:cancel",
f"{resolved_api_base}/{self.api_version}/interactions/{encode_path_segment(interaction_id)}:cancel",
{},
)

View file

@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
import httpx
from litellm.llms.base_llm._url_utils import encode_url_path
from litellm.llms.base_llm.vector_store.transformation import BaseVectorStoreConfig
from litellm.llms.gemini.common_utils import (
GeminiError,
@ -137,7 +138,7 @@ class GeminiVectorStoreConfig(BaseVectorStoreConfig):
api_key = litellm_params.get("api_key") or GeminiModelInfo.get_api_key()
if not api_key:
raise ValueError("GEMINI_API_KEY or GOOGLE_API_KEY is required")
url = f"{api_base}/models/{model}:generateContent"
url = f"{api_base}/models/{encode_url_path(model)}:generateContent"
# Build file_search tool configuration (using snake_case as per Gemini docs)
file_search_config: Dict[str, Any] = {

View file

@ -7,6 +7,7 @@ from httpx._types import RequestFiles
import litellm
from litellm.constants import DEFAULT_GOOGLE_VIDEO_DURATION_SECONDS
from litellm.images.utils import ImageEditRequestUtils
from litellm.llms.base_llm._url_utils import encode_url_path
from litellm.llms.base_llm.videos.transformation import BaseVideoConfig
from litellm.secret_managers.main import get_secret_str
from litellm.types.llms.gemini import (
@ -245,7 +246,7 @@ class GeminiVideoConfig(BaseVideoConfig):
return api_base.rstrip("/")
model_name = model.replace("gemini/", "")
url = f"{api_base.rstrip('/')}/v1beta/models/{model_name}:predictLongRunning"
url = f"{api_base.rstrip('/')}/v1beta/models/{encode_url_path(model_name)}:predictLongRunning"
return url
@ -376,7 +377,7 @@ class GeminiVideoConfig(BaseVideoConfig):
GET https://generativelanguage.googleapis.com/v1beta/{operation_name}
"""
operation_name = extract_original_video_id(video_id)
url = f"{api_base.rstrip('/')}/v1beta/{operation_name}"
url = f"{api_base.rstrip('/')}/v1beta/{encode_url_path(operation_name)}"
params: Dict[str, Any] = {}
return url, params
@ -451,7 +452,7 @@ class GeminiVideoConfig(BaseVideoConfig):
"""
operation_name = extract_original_video_id(video_id)
status_url = f"{api_base.rstrip('/')}/v1beta/{operation_name}"
status_url = f"{api_base.rstrip('/')}/v1beta/{encode_url_path(operation_name)}"
client = litellm.module_level_client
status_response = client.get(url=status_url, headers=headers)
status_response.raise_for_status()

View file

@ -13,6 +13,7 @@ if TYPE_CHECKING:
else:
LoggingClass = Any
from litellm.llms.base_llm._url_utils import encode_url_path
from litellm.llms.base_llm.chat.transformation import BaseLLMException
from ...openai.chat.gpt_transformation import OpenAIGPTConfig
@ -114,7 +115,7 @@ class HuggingFaceChatConfig(OpenAIGPTConfig):
if "/" in remaining:
provider = first_part
if provider == "hf-inference":
route = f"{provider}/models/{model}/v1/chat/completions"
route = f"{provider}/models/{encode_url_path(model)}/v1/chat/completions"
elif provider == "novita":
route = f"{provider}/v3/openai/chat/completions"
elif provider == "fireworks-ai":

View file

@ -4,6 +4,7 @@ from typing import Literal, Optional, Union
import httpx
from litellm.llms.base_llm._url_utils import encode_url_path
from litellm.llms.base_llm.chat.transformation import BaseLLMException
HF_HUB_URL = "https://huggingface.co"
@ -76,7 +77,7 @@ def _fetch_inference_provider_mapping(model: str) -> dict:
if os.getenv("HUGGINGFACE_API_KEY"):
headers["Authorization"] = f"Bearer {os.getenv('HUGGINGFACE_API_KEY')}"
path = f"{HF_HUB_URL}/api/models/{model}"
path = f"{HF_HUB_URL}/api/models/{encode_url_path(model)}"
params = {"expand": ["inferenceProviderMapping"]}
try:

View file

@ -6,6 +6,7 @@ import httpx
import litellm
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.llms.base_llm._url_utils import encode_url_path
from litellm.llms.custom_httpx.http_handler import (
AsyncHTTPHandler,
HTTPHandler,
@ -40,7 +41,7 @@ def get_hf_task_embedding_for_model(
)
http_client = HTTPHandler(concurrent_limit=1)
model_info = http_client.get(url=f"{api_base}/api/models/{model}")
model_info = http_client.get(url=f"{api_base}/api/models/{encode_url_path(model)}")
model_info_dict = model_info.json()
@ -65,7 +66,9 @@ async def async_get_hf_task_embedding_for_model(
llm_provider=litellm.LlmProviders.HUGGINGFACE,
)
model_info = await http_client.get(url=f"{api_base}/api/models/{model}")
model_info = await http_client.get(
url=f"{api_base}/api/models/{encode_url_path(model)}"
)
model_info_dict = model_info.json()

View file

@ -15,6 +15,7 @@ from litellm.litellm_core_utils.prompt_templates.factory import (
prompt_factory,
)
from litellm.litellm_core_utils.streaming_handler import CustomStreamWrapper
from litellm.llms.base_llm._url_utils import encode_url_path
from litellm.llms.base_llm.chat.transformation import BaseConfig, BaseLLMException
from litellm.secret_managers.main import get_secret_str
from litellm.types.llms.openai import AllMessageValues
@ -345,7 +346,9 @@ class HuggingFaceEmbeddingConfig(BaseConfig):
elif "HUGGINGFACE_API_BASE" in os.environ:
completion_url = os.getenv("HUGGINGFACE_API_BASE", "")
else:
completion_url = f"https://api-inference.huggingface.co/models/{model}"
completion_url = (
f"https://api-inference.huggingface.co/models/{encode_url_path(model)}"
)
return completion_url

View file

@ -19,6 +19,7 @@ from openai.types.file_deleted import FileDeleted
import litellm
from litellm._logging import verbose_logger
from litellm.litellm_core_utils.prompt_templates.common_utils import extract_file_data
from litellm.llms.base_llm._url_utils import encode_path_segment
from litellm.llms.base_llm.chat.transformation import BaseLLMException
from litellm.llms.base_llm.files.transformation import (
BaseFilesConfig,
@ -306,7 +307,7 @@ class ManusFilesConfig(BaseFilesConfig):
optional_params=optional_params,
litellm_params=litellm_params,
)
return f"{api_base}/{file_id}", {}
return f"{api_base}/{encode_path_segment(file_id)}", {}
def transform_retrieve_file_response(
self,
@ -336,7 +337,7 @@ class ManusFilesConfig(BaseFilesConfig):
optional_params=optional_params,
litellm_params=litellm_params,
)
return f"{api_base}/{file_id}", {}
return f"{api_base}/{encode_path_segment(file_id)}", {}
def transform_delete_file_response(
self,
@ -422,7 +423,7 @@ class ManusFilesConfig(BaseFilesConfig):
optional_params=optional_params,
litellm_params=litellm_params,
)
return f"{api_base}/{file_id}/content", {}
return f"{api_base}/{encode_path_segment(file_id)}/content", {}
def transform_file_content_response(
self,

View file

@ -9,6 +9,7 @@ from litellm.litellm_core_utils.core_helpers import process_response_headers
from litellm.litellm_core_utils.llm_response_utils.convert_dict_to_response import (
_safe_convert_created_field,
)
from litellm.llms.base_llm._url_utils import encode_path_segment
from litellm.llms.openai.common_utils import OpenAIError
from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig
from litellm.secret_managers.main import get_secret_str
@ -270,7 +271,7 @@ class ManusResponsesAPIConfig(OpenAIResponsesAPIConfig):
Reference: https://open.manus.im/docs/openai-compatibility
"""
url = f"{api_base}/{response_id}"
url = f"{api_base}/{encode_path_segment(response_id)}"
data: Dict = {}
return url, data

View file

@ -6,6 +6,7 @@ from typing_extensions import Required, TypedDict
import litellm
from litellm._uuid import uuid
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.llms.base_llm._url_utils import encode_url_path
from litellm.llms.base_llm.chat.transformation import BaseLLMException
from litellm.llms.base_llm.rerank.transformation import BaseRerankConfig
from litellm.secret_managers.main import get_secret_str
@ -92,7 +93,7 @@ class NvidiaNimRerankConfig(BaseRerankConfig):
# Strip nvidia_nim/ prefix from model name if present
clean_model = self._get_clean_model_name(model)
return f"{api_base}/v1/retrieval/{clean_model}/reranking"
return f"{api_base}/v1/retrieval/{encode_url_path(clean_model)}/reranking"
def get_supported_cohere_rerank_params(self, model: str) -> list:
"""

View file

@ -16,6 +16,7 @@ from litellm.types.containers.main import (
)
from litellm.types.router import GenericLiteLLMParams
from ...base_llm._url_utils import encode_path_segment
from ...base_llm.containers.transformation import BaseContainerConfig
from .utils import join_container_api_base_path
@ -198,7 +199,9 @@ class OpenAIContainerConfig(BaseContainerConfig):
) -> Tuple[str, Dict]:
"""Transform the OpenAI container retrieve request."""
# For container retrieve, we just need to construct the URL
url = join_container_api_base_path(api_base, f"/{container_id}")
url = join_container_api_base_path(
api_base, f"/{encode_path_segment(container_id)}"
)
# No additional data needed for GET request
data: Dict[str, Any] = {}
@ -230,7 +233,9 @@ class OpenAIContainerConfig(BaseContainerConfig):
- DELETE /v1/containers/{container_id}
"""
# Construct the URL for container delete
url = join_container_api_base_path(api_base, f"/{container_id}")
url = join_container_api_base_path(
api_base, f"/{encode_path_segment(container_id)}"
)
# No data needed for DELETE request
data: Dict[str, Any] = {}
@ -267,7 +272,9 @@ class OpenAIContainerConfig(BaseContainerConfig):
- GET /v1/containers/{container_id}/files
"""
# Construct the URL for container files
url = join_container_api_base_path(api_base, f"/{container_id}/files")
url = join_container_api_base_path(
api_base, f"/{encode_path_segment(container_id)}/files"
)
# Prepare query parameters
params: Dict[str, Any] = {}
@ -312,7 +319,8 @@ class OpenAIContainerConfig(BaseContainerConfig):
"""
# Construct the URL for container file content
url = join_container_api_base_path(
api_base, f"/{container_id}/files/{file_id}/content"
api_base,
f"/{encode_path_segment(container_id)}/files/{encode_path_segment(file_id)}/content",
)
# No query parameters needed

View file

@ -7,6 +7,7 @@ from typing import Any, Dict, Optional, Tuple
import httpx
from litellm._logging import verbose_logger
from litellm.llms.base_llm._url_utils import encode_path_segment
from litellm.llms.base_llm.evals.transformation import (
BaseEvalsAPIConfig,
LiteLLMLoggingObj,
@ -76,7 +77,7 @@ class OpenAIEvalsConfig(BaseEvalsAPIConfig):
api_base = "https://api.openai.com"
if eval_id:
return f"{api_base}/v1/evals/{eval_id}"
return f"{api_base}/v1/evals/{encode_path_segment(eval_id)}"
return f"{api_base}/v1/{endpoint}"
def transform_create_eval_request(
@ -276,7 +277,7 @@ class OpenAIEvalsConfig(BaseEvalsAPIConfig):
if litellm_params and litellm_params.api_base:
api_base = litellm_params.api_base
url = f"{api_base}/v1/evals/{eval_id}/runs"
url = f"{api_base}/v1/evals/{encode_path_segment(eval_id)}/runs"
# Build request body
request_body = {k: v for k, v in create_request.items() if v is not None}
@ -310,7 +311,7 @@ class OpenAIEvalsConfig(BaseEvalsAPIConfig):
if litellm_params and litellm_params.api_base:
api_base = litellm_params.api_base
url = f"{api_base}/v1/evals/{eval_id}/runs"
url = f"{api_base}/v1/evals/{encode_path_segment(eval_id)}/runs"
# Build query parameters
query_params: Dict[str, Any] = {}
@ -350,7 +351,7 @@ class OpenAIEvalsConfig(BaseEvalsAPIConfig):
headers: dict,
) -> Tuple[str, Dict]:
"""Transform get run request for OpenAI"""
url = f"{api_base}/v1/evals/{eval_id}/runs/{run_id}"
url = f"{api_base}/v1/evals/{encode_path_segment(eval_id)}/runs/{encode_path_segment(run_id)}"
verbose_logger.debug("Get run request - URL: %s", url)
@ -376,7 +377,7 @@ class OpenAIEvalsConfig(BaseEvalsAPIConfig):
headers: dict,
) -> Tuple[str, Dict, Dict]:
"""Transform cancel run request for OpenAI"""
url = f"{api_base}/v1/evals/{eval_id}/runs/{run_id}/cancel"
url = f"{api_base}/v1/evals/{encode_path_segment(eval_id)}/runs/{encode_path_segment(run_id)}/cancel"
# Empty body for cancel request
request_body: Dict[str, Any] = {}
@ -405,7 +406,7 @@ class OpenAIEvalsConfig(BaseEvalsAPIConfig):
headers: dict,
) -> Tuple[str, Dict, Dict]:
"""Transform delete run request for OpenAI"""
url = f"{api_base}/v1/evals/{eval_id}/runs/{run_id}"
url = f"{api_base}/v1/evals/{encode_path_segment(eval_id)}/runs/{encode_path_segment(run_id)}"
# Empty body for delete request
request_body: Dict[str, Any] = {}

View file

@ -10,6 +10,7 @@ from litellm.litellm_core_utils.core_helpers import process_response_headers
from litellm.litellm_core_utils.llm_response_utils.convert_dict_to_response import (
_safe_convert_created_field,
)
from litellm.llms.base_llm._url_utils import encode_path_segment
from litellm.llms.base_llm.responses.transformation import BaseResponsesAPIConfig
from litellm.secret_managers.main import get_secret_str
from litellm.types.llms.openai import *
@ -421,7 +422,7 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig):
OpenAI API expects the following request
- DELETE /v1/responses/{response_id}
"""
url = f"{api_base}/{response_id}"
url = f"{api_base}/{encode_path_segment(response_id)}"
data: Dict = {}
return url, data
@ -457,7 +458,7 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig):
OpenAI API expects the following request
- GET /v1/responses/{response_id}
"""
url = f"{api_base}/{response_id}"
url = f"{api_base}/{encode_path_segment(response_id)}"
data: Dict = {}
return url, data
@ -498,7 +499,7 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig):
limit: int = 20,
order: Literal["asc", "desc"] = "desc",
) -> Tuple[str, Dict]:
url = f"{api_base}/{response_id}/input_items"
url = f"{api_base}/{encode_path_segment(response_id)}/input_items"
params: Dict[str, Any] = {}
if after is not None:
params["after"] = after
@ -540,7 +541,7 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig):
OpenAI API expects the following request
- POST /v1/responses/{response_id}/cancel
"""
url = f"{api_base}/{response_id}/cancel"
url = f"{api_base}/{encode_path_segment(response_id)}/cancel"
data: Dict = {}
return url, data

View file

@ -3,6 +3,7 @@ from typing import Any, Dict, Optional, Tuple, cast
import httpx
import litellm
from litellm.llms.base_llm._url_utils import encode_path_segment
from litellm.llms.base_llm.vector_store_files.transformation import (
BaseVectorStoreFilesConfig,
)
@ -98,7 +99,7 @@ class OpenAIVectorStoreFilesConfig(BaseVectorStoreFilesConfig):
or "https://api.openai.com/v1"
)
base_url = base_url.rstrip("/")
return f"{base_url}/vector_stores/{vector_store_id}/files"
return f"{base_url}/vector_stores/{encode_path_segment(vector_store_id)}/files"
def transform_create_vector_store_file_request(
self,
@ -163,7 +164,7 @@ class OpenAIVectorStoreFilesConfig(BaseVectorStoreFilesConfig):
file_id: str,
api_base: str,
) -> Tuple[str, Dict[str, Any]]:
return f"{api_base}/{file_id}", {}
return f"{api_base}/{encode_path_segment(file_id)}", {}
def transform_retrieve_vector_store_file_response(
self,
@ -186,7 +187,7 @@ class OpenAIVectorStoreFilesConfig(BaseVectorStoreFilesConfig):
file_id: str,
api_base: str,
) -> Tuple[str, Dict[str, Any]]:
return f"{api_base}/{file_id}/content", {}
return f"{api_base}/{encode_path_segment(file_id)}/content", {}
def transform_retrieve_vector_store_file_content_response(
self,
@ -218,7 +219,7 @@ class OpenAIVectorStoreFilesConfig(BaseVectorStoreFilesConfig):
payload["attributes"] = filtered_attributes
else:
payload.pop("attributes", None)
return f"{api_base}/{file_id}", payload
return f"{api_base}/{encode_path_segment(file_id)}", payload
def transform_update_vector_store_file_response(
self,
@ -241,7 +242,7 @@ class OpenAIVectorStoreFilesConfig(BaseVectorStoreFilesConfig):
file_id: str,
api_base: str,
) -> Tuple[str, Dict[str, Any]]:
return f"{api_base}/{file_id}", {}
return f"{api_base}/{encode_path_segment(file_id)}", {}
def transform_delete_vector_store_file_response(
self,

View file

@ -3,6 +3,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, cast
import httpx
import litellm
from litellm.llms.base_llm._url_utils import encode_path_segment
from litellm.llms.base_llm.vector_store.transformation import BaseVectorStoreConfig
from litellm.secret_managers.main import get_secret_str
from litellm.types.router import GenericLiteLLMParams
@ -107,7 +108,7 @@ class OpenAIVectorStoreConfig(BaseVectorStoreConfig):
litellm_logging_obj: LiteLLMLoggingObj,
litellm_params: dict,
) -> Tuple[str, Dict]:
url = f"{api_base}/{vector_store_id}/search"
url = f"{api_base}/{encode_path_segment(vector_store_id)}/search"
typed_request_body = VectorStoreSearchRequest(
query=query,
filters=vector_store_search_optional_params.get("filters", None),

View file

@ -1,11 +1,13 @@
import mimetypes
from io import BufferedReader, BytesIO
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, cast
from urllib.parse import quote
import httpx
from httpx._types import RequestFiles
import litellm
from litellm.llms.base_llm._url_utils import encode_path_segment
from litellm.llms.base_llm.videos.transformation import BaseVideoConfig
from litellm.llms.openai.image_edit.transformation import ImageEditRequestUtils
from litellm.secret_managers.main import get_secret_str
@ -222,9 +224,9 @@ class OpenAIVideoConfig(BaseVideoConfig):
original_video_id = extract_original_video_id(video_id)
# Construct the URL for video content download
url = f"{api_base.rstrip('/')}/{original_video_id}/content"
url = f"{api_base.rstrip('/')}/{encode_path_segment(original_video_id)}/content"
if variant is not None:
url = f"{url}?variant={variant}"
url = f"{url}?variant={quote(str(variant), safe='')}"
# No additional data needed for GET content request
data: Dict[str, Any] = {}
@ -249,7 +251,7 @@ class OpenAIVideoConfig(BaseVideoConfig):
original_video_id = extract_original_video_id(video_id)
# Construct the URL for video remix
url = f"{api_base.rstrip('/')}/{original_video_id}/remix"
url = f"{api_base.rstrip('/')}/{encode_path_segment(original_video_id)}/remix"
# Prepare the request data
data = {"prompt": prompt}
@ -393,7 +395,7 @@ class OpenAIVideoConfig(BaseVideoConfig):
original_video_id = extract_original_video_id(video_id)
# Construct the URL for video delete
url = f"{api_base.rstrip('/')}/{original_video_id}"
url = f"{api_base.rstrip('/')}/{encode_path_segment(original_video_id)}"
# No data needed for DELETE request
data: Dict[str, Any] = {}
@ -429,7 +431,7 @@ class OpenAIVideoConfig(BaseVideoConfig):
original_video_id = extract_original_video_id(video_id)
# For video retrieve, we just need to construct the URL
url = f"{api_base.rstrip('/')}/{original_video_id}"
url = f"{api_base.rstrip('/')}/{encode_path_segment(original_video_id)}"
# No additional data needed for GET request
data: Dict[str, Any] = {}
@ -494,7 +496,7 @@ class OpenAIVideoConfig(BaseVideoConfig):
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> Tuple[str, Dict]:
url = f"{api_base.rstrip('/')}/characters/{character_id}"
url = f"{api_base.rstrip('/')}/characters/{encode_path_segment(character_id)}"
return url, {}
def transform_video_get_character_response(

View file

@ -1,5 +1,6 @@
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
from litellm.llms.base_llm._url_utils import encode_path_segment
from litellm.llms.openai.vector_stores.transformation import OpenAIVectorStoreConfig
from litellm.secret_managers.main import get_secret_str
from litellm.types.router import GenericLiteLLMParams
@ -81,7 +82,7 @@ class PGVectorStoreConfig(OpenAIVectorStoreConfig):
litellm_logging_obj: LiteLLMLoggingObj,
litellm_params: dict,
) -> Tuple[str, Dict]:
url = f"{api_base}/{vector_store_id}/search"
url = f"{api_base}/{encode_path_segment(vector_store_id)}/search"
_, request_body = super().transform_search_vector_store_request(
vector_store_id=vector_store_id,
query=query,

View file

@ -17,6 +17,7 @@ from litellm.litellm_core_utils.prompt_templates.factory import (
custom_prompt,
prompt_factory,
)
from litellm.llms.base_llm._url_utils import encode_path_segment
from litellm.llms.custom_httpx.http_handler import (
AsyncHTTPHandler,
get_async_httpx_client,
@ -254,7 +255,7 @@ class PredibaseChatCompletion:
elif "PREDIBASE_API_BASE" in os.environ:
base_url = os.getenv("PREDIBASE_API_BASE", "")
completion_url = f"{base_url}/{tenant_id}/deployments/v2/llms/{model}"
completion_url = f"{base_url}/{encode_path_segment(tenant_id)}/deployments/v2/llms/{encode_path_segment(model)}"
if optional_params.get("stream", False) is True:
completion_url += "/generate_stream"

View file

@ -13,6 +13,7 @@ Model name format:
from typing import List, Optional, Tuple
import litellm
from litellm.llms.base_llm._url_utils import encode_path_segment
from litellm.llms.openai.openai import OpenAIConfig
from litellm.secret_managers.main import get_secret, get_secret_str
from litellm.types.llms.openai import AllMessageValues
@ -126,14 +127,11 @@ class RAGFlowConfig(OpenAIConfig):
api_base = api_base[:-3] # Remove /v1
# Construct the RAGFlow-specific path
encoded_entity_id = encode_path_segment(entity_id)
if endpoint_type == "chat":
path = f"/api/v1/chats_openai/{entity_id}/chat/completions"
path = f"/api/v1/chats_openai/{encoded_entity_id}/chat/completions"
else: # agent
path = f"/api/v1/agents_openai/{entity_id}/chat/completions"
# Ensure path starts with /
if not path.startswith("/"):
path = "/" + path
path = f"/api/v1/agents_openai/{encoded_entity_id}/chat/completions"
return f"{api_base}{path}"

View file

@ -9,6 +9,7 @@ from litellm.constants import (
RUNWAYML_DEFAULT_API_VERSION,
RUNWAYML_POLLING_TIMEOUT,
)
from litellm.llms.base_llm._url_utils import encode_path_segment
from litellm.llms.base_llm.image_generation.transformation import (
BaseImageGenerationConfig,
)
@ -222,7 +223,7 @@ class RunwayMLImageGenerationConfig(BaseImageGenerationConfig):
# Build task status URL
api_base = api_base.rstrip("/")
task_url = f"{api_base}/v1/tasks/{task_id}"
task_url = f"{api_base}/v1/tasks/{encode_path_segment(task_id)}"
verbose_logger.debug(f"Polling RunwayML task: {task_url}")
@ -271,7 +272,7 @@ class RunwayMLImageGenerationConfig(BaseImageGenerationConfig):
# Build task status URL
api_base = api_base.rstrip("/")
task_url = f"{api_base}/v1/tasks/{task_id}"
task_url = f"{api_base}/v1/tasks/{encode_path_segment(task_id)}"
verbose_logger.debug(f"Polling RunwayML task (async): {task_url}")

View file

@ -16,6 +16,7 @@ from litellm.constants import (
RUNWAYML_DEFAULT_API_VERSION,
RUNWAYML_POLLING_TIMEOUT,
)
from litellm.llms.base_llm._url_utils import encode_path_segment
from litellm.llms.base_llm.text_to_speech.transformation import (
BaseTextToSpeechConfig,
TextToSpeechRequestData,
@ -312,7 +313,7 @@ class RunwayMLTextToSpeechConfig(BaseTextToSpeechConfig):
# Build task status URL
api_base = api_base.rstrip("/")
task_url = f"{api_base}/v1/tasks/{task_id}"
task_url = f"{api_base}/v1/tasks/{encode_path_segment(task_id)}"
verbose_logger.debug(f"Polling RunwayML TTS task: {task_url}")
@ -360,7 +361,7 @@ class RunwayMLTextToSpeechConfig(BaseTextToSpeechConfig):
# Build task status URL
api_base = api_base.rstrip("/")
task_url = f"{api_base}/v1/tasks/{task_id}"
task_url = f"{api_base}/v1/tasks/{encode_path_segment(task_id)}"
verbose_logger.debug(f"Polling RunwayML TTS task (async): {task_url}")

View file

@ -6,6 +6,7 @@ from httpx._types import RequestFiles
import litellm
from litellm.constants import RUNWAYML_DEFAULT_API_VERSION
from litellm.llms.base_llm._url_utils import encode_path_segment
from litellm.llms.base_llm.chat.transformation import BaseLLMException
from litellm.llms.base_llm.videos.transformation import BaseVideoConfig
from litellm.llms.custom_httpx.http_handler import (
@ -336,7 +337,7 @@ class RunwayMLVideoConfig(BaseVideoConfig):
original_video_id = extract_original_video_id(video_id)
# Get task status to retrieve video URL
url = f"{api_base}/tasks/{original_video_id}"
url = f"{api_base}/tasks/{encode_path_segment(original_video_id)}"
params: Dict[str, Any] = {}
@ -497,7 +498,7 @@ class RunwayMLVideoConfig(BaseVideoConfig):
original_video_id = extract_original_video_id(video_id)
# Construct the URL for task cancellation
url = f"{api_base}/tasks/{original_video_id}/cancel"
url = f"{api_base}/tasks/{encode_path_segment(original_video_id)}/cancel"
data: Dict[str, Any] = {}
@ -535,7 +536,7 @@ class RunwayMLVideoConfig(BaseVideoConfig):
original_video_id = extract_original_video_id(video_id)
# Construct the full URL for task status retrieval
url = f"{api_base}/tasks/{original_video_id}"
url = f"{api_base}/tasks/{encode_path_segment(original_video_id)}"
# Empty dict for GET request (no body)
data: Dict[str, Any] = {}

View file

@ -4,6 +4,7 @@ from typing import Any, Coroutine, Dict, Optional, Union
import httpx
import litellm
from litellm.llms.base_llm._url_utils import encode_path_segment
from litellm.llms.custom_httpx.http_handler import (
_get_httpx_client,
get_async_httpx_client,
@ -169,7 +170,7 @@ class VertexAIBatchPrediction(VertexLLM):
)
# Append batch_id to the URL
default_api_base = f"{default_api_base}/{batch_id}"
default_api_base = f"{default_api_base}/{encode_path_segment(batch_id)}"
if len(default_api_base.split(":")) > 1:
endpoint = default_api_base.split(":")[-1]
@ -401,7 +402,9 @@ class VertexAIBatchPrediction(VertexLLM):
vertex_project=vertex_project or project_id,
)
retrieve_api_base_default = f"{default_api_base}/{batch_id}"
retrieve_api_base_default = (
f"{default_api_base}/{encode_path_segment(batch_id)}"
)
cancel_api_base_default = f"{retrieve_api_base_default}:cancel"
_, api_base = self._check_custom_proxy(

View file

@ -9,6 +9,7 @@ import litellm
from litellm._logging import verbose_logger
from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH
from litellm.litellm_core_utils.prompt_templates.common_utils import unpack_defs
from litellm.llms.base_llm._url_utils import encode_path_segment, encode_url_path
from litellm.llms.base_llm.base_utils import BaseLLMModelInfo, BaseTokenCounter
from litellm.llms.base_llm.chat.transformation import BaseLLMException
from litellm.types.llms.openai import AllMessageValues
@ -275,11 +276,14 @@ def _get_embedding_url(
endpoint = "embedContent" if uses_embed_content else "predict"
base_url = get_vertex_base_url(vertex_location)
proj = encode_path_segment(vertex_project)
loc = encode_path_segment(vertex_location)
model_seg = encode_url_path(model)
if model.isdigit():
url = f"{base_url}/{vertex_api_version}/projects/{vertex_project}/locations/{vertex_location}/endpoints/{model}:{endpoint}"
url = f"{base_url}/{vertex_api_version}/projects/{proj}/locations/{loc}/endpoints/{model_seg}:{endpoint}"
else:
url = f"{base_url}/v1/projects/{vertex_project}/locations/{vertex_location}/publishers/google/models/{model}:{endpoint}"
url = f"{base_url}/v1/projects/{proj}/locations/{loc}/publishers/google/models/{model_seg}:{endpoint}"
return url, endpoint
@ -297,6 +301,10 @@ def _get_vertex_url(
model = litellm.VertexGeminiConfig.get_model_for_vertex_ai_url(model=model)
proj = encode_path_segment(vertex_project)
loc = encode_path_segment(vertex_location)
model_seg = encode_url_path(model)
if mode == "chat":
### SET RUNTIME ENDPOINT ###
endpoint = "generateContent"
@ -310,10 +318,10 @@ def _get_vertex_url(
# send to this url: url = f"{base_url}/{version}/projects/{vertex_project}/locations/{vertex_location}/endpoints/{model}:{endpoint}"
if model.isdigit():
# It's a fine-tuned Gemini model - use endpoints/ path
url = f"{base_url}/{vertex_api_version}/projects/{vertex_project}/locations/{vertex_location}/endpoints/{model}:{endpoint}"
url = f"{base_url}/{vertex_api_version}/projects/{proj}/locations/{loc}/endpoints/{model_seg}:{endpoint}"
else:
# Regular model - use publishers/google/models/ path
url = f"{base_url}/{vertex_api_version}/projects/{vertex_project}/locations/{vertex_location}/publishers/google/models/{model}:{endpoint}"
url = f"{base_url}/{vertex_api_version}/projects/{proj}/locations/{loc}/publishers/google/models/{model_seg}:{endpoint}"
if stream is True:
url += "?alt=sse"
@ -329,14 +337,14 @@ def _get_vertex_url(
base_url = get_vertex_base_url(vertex_location)
if model.isdigit():
# Numeric model -> custom endpoint
url = f"{base_url}/{vertex_api_version}/projects/{vertex_project}/locations/{vertex_location}/endpoints/{model}:{endpoint}"
url = f"{base_url}/{vertex_api_version}/projects/{proj}/locations/{loc}/endpoints/{model_seg}:{endpoint}"
else:
# Regular model -> publisher model
url = f"{base_url}/v1/projects/{vertex_project}/locations/{vertex_location}/publishers/google/models/{model}:{endpoint}"
url = f"{base_url}/v1/projects/{proj}/locations/{loc}/publishers/google/models/{model_seg}:{endpoint}"
elif mode == "count_tokens":
endpoint = "countTokens"
base_url = get_vertex_base_url(vertex_location)
url = f"{base_url}/{vertex_api_version}/projects/{vertex_project}/locations/{vertex_location}/publishers/google/models/{model}:{endpoint}"
url = f"{base_url}/{vertex_api_version}/projects/{proj}/locations/{loc}/publishers/google/models/{model_seg}:{endpoint}"
if not url or not endpoint:
raise ValueError(f"Unable to get vertex url/endpoint for mode: {mode}")
return url, endpoint
@ -357,7 +365,7 @@ def _get_gemini_url(
VertexGeminiConfig,
)
_gemini_model_name = "models/{}".format(model)
_gemini_model_name = "models/{}".format(encode_url_path(model))
api_version = (
"v1alpha" if VertexGeminiConfig._is_gemini_3_or_newer(model) else "v1beta"
)

View file

@ -9,6 +9,7 @@ from httpx._types import RequestFiles
import litellm
from litellm.images.utils import ImageEditRequestUtils
from litellm.llms.base_llm._url_utils import encode_path_segment, encode_url_path
from litellm.llms.base_llm.image_edit.transformation import BaseImageEditConfig
from litellm.llms.vertex_ai.common_utils import get_vertex_base_url
from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import VertexLLM
@ -161,7 +162,7 @@ class VertexAIGeminiImageEditConfig(BaseImageEditConfig, VertexLLM):
base_url = get_vertex_base_url(vertex_location)
return f"{base_url}/v1/projects/{vertex_project}/locations/{vertex_location}/publishers/google/models/{model_name}:generateContent"
return f"{base_url}/v1/projects/{encode_path_segment(vertex_project)}/locations/{encode_path_segment(vertex_location)}/publishers/google/models/{encode_url_path(model_name)}:generateContent"
def transform_image_edit_request( # type: ignore[override]
self,

View file

@ -10,6 +10,7 @@ from httpx._types import RequestFiles
import litellm
from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH
from litellm.llms.base_llm._url_utils import encode_path_segment, encode_url_path
from litellm.llms.base_llm.image_edit.transformation import BaseImageEditConfig
from litellm.llms.vertex_ai.common_utils import get_vertex_base_url
from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import VertexLLM
@ -161,7 +162,7 @@ class VertexAIImagenImageEditConfig(BaseImageEditConfig, VertexLLM):
else:
base_url = get_vertex_base_url(vertex_location)
return f"{base_url}/v1/projects/{vertex_project}/locations/{vertex_location}/publishers/google/models/{model_name}:predict"
return f"{base_url}/v1/projects/{encode_path_segment(vertex_project)}/locations/{encode_path_segment(vertex_location)}/publishers/google/models/{encode_url_path(model_name)}:predict"
def transform_image_edit_request( # type: ignore[override]
self,

View file

@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional
import httpx
import litellm
from litellm.llms.base_llm._url_utils import encode_path_segment, encode_url_path
from litellm.llms.base_llm.image_generation.transformation import (
BaseImageGenerationConfig,
)
@ -162,7 +163,7 @@ class VertexAIGeminiImageGenerationConfig(BaseImageGenerationConfig, VertexLLM):
base_url = get_vertex_base_url(vertex_location)
return f"{base_url}/v1/projects/{vertex_project}/locations/{vertex_location}/publishers/google/models/{model_name}:generateContent"
return f"{base_url}/v1/projects/{encode_path_segment(vertex_project)}/locations/{encode_path_segment(vertex_location)}/publishers/google/models/{encode_url_path(model_name)}:generateContent"
def validate_environment(
self,

View file

@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Any, List, Optional
import httpx
import litellm
from litellm.llms.base_llm._url_utils import encode_path_segment, encode_url_path
from litellm.llms.base_llm.image_generation.transformation import (
BaseImageGenerationConfig,
)
@ -148,7 +149,7 @@ class VertexAIImagenImageGenerationConfig(BaseImageGenerationConfig, VertexLLM):
base_url = get_vertex_base_url(vertex_location)
return f"{base_url}/v1/projects/{vertex_project}/locations/{vertex_location}/publishers/google/models/{model_name}:predict"
return f"{base_url}/v1/projects/{encode_path_segment(vertex_project)}/locations/{encode_path_segment(vertex_location)}/publishers/google/models/{encode_url_path(model_name)}:predict"
def validate_environment(
self,

View file

@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Any, Dict, Optional
import httpx
from litellm._logging import verbose_logger
from litellm.llms.base_llm._url_utils import encode_path_segment
from litellm.llms.base_llm.ocr.transformation import (
BaseOCRConfig,
DocumentType,
@ -125,7 +126,7 @@ class VertexAIDeepSeekOCRConfig(BaseOCRConfig):
# Vertex AI DeepSeek OCR endpoint format
# Format: https://{region}-aiplatform.googleapis.com/v1/projects/{project}/locations/{region}/endpoints/openapi/chat/completions
return f"{api_base}/v1/projects/{vertex_project}/locations/{vertex_location}/endpoints/openapi/chat/completions"
return f"{api_base}/v1/projects/{encode_path_segment(vertex_project)}/locations/{encode_path_segment(vertex_location)}/endpoints/openapi/chat/completions"
def transform_ocr_request(
self,

View file

@ -9,6 +9,7 @@ from litellm.litellm_core_utils.prompt_templates.image_handling import (
async_convert_url_to_base64,
convert_url_to_base64,
)
from litellm.llms.base_llm._url_utils import encode_path_segment, encode_url_path
from litellm.llms.base_llm.ocr.transformation import DocumentType, OCRRequestData
from litellm.llms.mistral.ocr.transformation import MistralOCRConfig
from litellm.llms.vertex_ai.common_utils import get_vertex_base_url
@ -121,7 +122,7 @@ class VertexAIOCRConfig(MistralOCRConfig):
# Vertex AI OCR endpoint format for Mistral publisher
# Format: https://{region}-aiplatform.googleapis.com/v1/projects/{project}/locations/{region}/publishers/mistralai/models/{model}:rawPredict
return f"{api_base}/v1/projects/{vertex_project}/locations/{vertex_location}/publishers/mistralai/models/{model}:rawPredict"
return f"{api_base}/v1/projects/{encode_path_segment(vertex_project)}/locations/{encode_path_segment(vertex_location)}/publishers/mistralai/models/{encode_url_path(model)}:rawPredict"
def _convert_url_to_data_uri_sync(self, url: str) -> str:
"""

View file

@ -8,6 +8,7 @@ from typing import Any, Dict, Optional
from litellm._logging import verbose_logger
from litellm.constants import DEFAULT_CHUNK_OVERLAP, DEFAULT_CHUNK_SIZE
from litellm.llms.base_llm._url_utils import encode_path_segment
from litellm.llms.vertex_ai.common_utils import get_vertex_base_url
from litellm.llms.vertex_ai.vertex_llm_base import VertexBase
from litellm.types.rag import RAGChunkingStrategy
@ -39,7 +40,7 @@ class VertexAIRAGTransformation(VertexBase):
Vertex AI RAG Engine primarily uses gRPC-based SDK.
"""
base_url = get_vertex_base_url(vertex_location)
return f"{base_url}/v1/projects/{vertex_project}/locations/{vertex_location}/ragCorpora/{corpus_id}:importRagFiles"
return f"{base_url}/v1/projects/{encode_path_segment(vertex_project)}/locations/{encode_path_segment(vertex_location)}/ragCorpora/{encode_path_segment(corpus_id)}:importRagFiles"
def get_retrieve_contexts_url(
self,
@ -48,7 +49,7 @@ class VertexAIRAGTransformation(VertexBase):
) -> str:
"""Get the URL for retrieving contexts (search)."""
base_url = get_vertex_base_url(vertex_location)
return f"{base_url}/v1/projects/{vertex_project}/locations/{vertex_location}:retrieveContexts"
return f"{base_url}/v1/projects/{encode_path_segment(vertex_project)}/locations/{encode_path_segment(vertex_location)}:retrieveContexts"
def transform_chunking_strategy_to_vertex_format(
self,

View file

@ -8,6 +8,7 @@ their respective publisher-specific count-tokens endpoints.
from typing import Any, Dict, Optional
from litellm.llms.base_llm._url_utils import encode_path_segment
from litellm.llms.custom_httpx.http_handler import get_async_httpx_client
from litellm.llms.vertex_ai.common_utils import get_vertex_base_url
from litellm.llms.vertex_ai.vertex_llm_base import VertexBase
@ -72,10 +73,7 @@ class VertexAIPartnerModelsTokenCounter(VertexBase):
# Construct the count-tokens endpoint
# Format: /v1/projects/{project}/locations/{location}/publishers/{publisher}/models/count-tokens:rawPredict
endpoint = (
f"{base_url}/v1/projects/{project_id}/locations/{vertex_location}/"
f"publishers/{publisher}/models/count-tokens:rawPredict"
)
endpoint = f"{base_url}/v1/projects/{encode_path_segment(project_id)}/locations/{encode_path_segment(vertex_location)}/publishers/{encode_path_segment(publisher)}/models/count-tokens:rawPredict"
return endpoint

View file

@ -11,6 +11,7 @@ from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, Tuple
import litellm
from litellm._logging import verbose_logger
from litellm.litellm_core_utils.asyncify import asyncify
from litellm.llms.base_llm._url_utils import encode_path_segment, encode_url_path
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler
from litellm.secret_managers.main import get_secret_str
from litellm.types.llms.vertex_ai import VERTEX_CREDENTIALS_TYPES, VertexPartnerProvider
@ -273,23 +274,20 @@ class VertexBase:
if api_base is None:
api_base = get_vertex_base_url(vertex_location)
proj = encode_path_segment(vertex_project)
loc = encode_path_segment(vertex_location)
model_seg = encode_url_path(model)
if partner == VertexPartnerProvider.llama:
return f"{api_base}/v1/projects/{vertex_project}/locations/{vertex_location}/endpoints/openapi/chat/completions"
return f"{api_base}/v1/projects/{proj}/locations/{loc}/endpoints/openapi/chat/completions"
elif partner == VertexPartnerProvider.mistralai:
if stream:
return f"{api_base}/v1/projects/{vertex_project}/locations/{vertex_location}/publishers/mistralai/models/{model}:streamRawPredict"
else:
return f"{api_base}/v1/projects/{vertex_project}/locations/{vertex_location}/publishers/mistralai/models/{model}:rawPredict"
action = "streamRawPredict" if stream else "rawPredict"
return f"{api_base}/v1/projects/{proj}/locations/{loc}/publishers/mistralai/models/{model_seg}:{action}"
elif partner == VertexPartnerProvider.ai21:
if stream:
return f"{api_base}/v1beta1/projects/{vertex_project}/locations/{vertex_location}/publishers/ai21/models/{model}:streamRawPredict"
else:
return f"{api_base}/v1beta1/projects/{vertex_project}/locations/{vertex_location}/publishers/ai21/models/{model}:rawPredict"
action = "streamRawPredict" if stream else "rawPredict"
return f"{api_base}/v1beta1/projects/{proj}/locations/{loc}/publishers/ai21/models/{model_seg}:{action}"
elif partner == VertexPartnerProvider.claude:
if stream:
return f"{api_base}/v1/projects/{vertex_project}/locations/{vertex_location}/publishers/anthropic/models/{model}:streamRawPredict"
else:
return f"{api_base}/v1/projects/{vertex_project}/locations/{vertex_location}/publishers/anthropic/models/{model}:rawPredict"
action = "streamRawPredict" if stream else "rawPredict"
return f"{api_base}/v1/projects/{proj}/locations/{loc}/publishers/anthropic/models/{model_seg}:{action}"
def get_complete_vertex_url(
self,

View file

@ -14,6 +14,7 @@ from httpx._types import RequestFiles
from litellm.constants import DEFAULT_GOOGLE_VIDEO_DURATION_SECONDS
from litellm.images.utils import ImageEditRequestUtils
from litellm.llms.base_llm._url_utils import encode_path_segment, encode_url_path
from litellm.llms.base_llm.videos.transformation import BaseVideoConfig
from litellm.llms.vertex_ai.common_utils import (
_convert_vertex_datetime_to_openai_datetime,
@ -238,7 +239,7 @@ class VertexAIVideoConfig(BaseVideoConfig, VertexBase):
else:
base_url = get_vertex_base_url(vertex_location)
url = f"{base_url}/v1/projects/{vertex_project}/locations/{vertex_location}/publishers/google/models/{model_name}"
url = f"{base_url}/v1/projects/{encode_path_segment(vertex_project)}/locations/{encode_path_segment(vertex_location)}/publishers/google/models/{encode_url_path(model_name)}"
return url
@ -406,7 +407,7 @@ class VertexAIVideoConfig(BaseVideoConfig, VertexBase):
# Construct the full URL including model ID
# URL format: https://LOCATION-aiplatform.googleapis.com/v1/projects/PROJECT/locations/LOCATION/publishers/google/models/MODEL:fetchPredictOperation
# Strip trailing slashes from api_base and append model
url = f"{api_base.rstrip('/')}/{model}:fetchPredictOperation"
url = f"{api_base.rstrip('/')}/{encode_url_path(model)}:fetchPredictOperation"
# Request body contains the operation name
params = {"operationName": operation_name}

View file

@ -20,6 +20,7 @@ from litellm.litellm_core_utils.core_helpers import process_response_headers
from litellm.litellm_core_utils.llm_response_utils.convert_dict_to_response import (
_safe_convert_created_field,
)
from litellm.llms.base_llm._url_utils import encode_path_segment
from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig
from litellm.secret_managers.main import get_secret_str
from litellm.types.llms.openai import (
@ -300,7 +301,7 @@ class VolcEngineResponsesAPIConfig(OpenAIResponsesAPIConfig):
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> Tuple[str, Dict]:
url = f"{api_base}/{response_id}"
url = f"{api_base}/{encode_path_segment(response_id)}"
data: Dict = {}
return url, data
@ -333,7 +334,7 @@ class VolcEngineResponsesAPIConfig(OpenAIResponsesAPIConfig):
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> Tuple[str, Dict]:
url = f"{api_base}/{response_id}"
url = f"{api_base}/{encode_path_segment(response_id)}"
data: Dict = {}
return url, data
@ -372,7 +373,7 @@ class VolcEngineResponsesAPIConfig(OpenAIResponsesAPIConfig):
limit: int = 20,
order: Literal["asc", "desc"] = "desc",
) -> Tuple[str, Dict]:
url = f"{api_base}/{response_id}/input_items"
url = f"{api_base}/{encode_path_segment(response_id)}/input_items"
params: Dict[str, Any] = {}
if after is not None:
params["after"] = after
@ -408,7 +409,7 @@ class VolcEngineResponsesAPIConfig(OpenAIResponsesAPIConfig):
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> Tuple[str, Dict]:
url = f"{api_base}/{response_id}/cancel"
url = f"{api_base}/{encode_path_segment(response_id)}/cancel"
data: Dict = {}
return url, data

View file

@ -10,6 +10,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, cast
from litellm._logging import verbose_logger
from litellm.llms.base_llm._url_utils import encode_path_segment
from litellm.llms.custom_httpx.http_handler import (
get_async_httpx_client,
httpxSpecialProvider,
@ -231,7 +232,7 @@ class GeminiRAGIngestion(BaseRAGIngestion):
# base_url is like: https://generativelanguage.googleapis.com/v1beta
# We need: https://generativelanguage.googleapis.com/upload/v1beta/{store_id}:uploadToFileSearchStore
api_base = base_url.replace("/v1beta", "") # Get base without version
url = f"{api_base}/upload/v1beta/{vector_store_id}:uploadToFileSearchStore"
url = f"{api_base}/upload/v1beta/{encode_path_segment(vector_store_id)}:uploadToFileSearchStore"
# Build request body with chunking config and metadata if provided
request_body: Dict[str, Any] = {"displayName": filename}

View file

@ -0,0 +1,710 @@
"""Tests for ``encode_path_segment`` / ``encode_url_path`` and provider call sites."""
import pytest
from litellm.llms.base_llm._url_utils import encode_path_segment, encode_url_path
SAMPLE_INPUT = "../../v1/messages/batches"
SAMPLE_INPUT_ENCODED = "..%2F..%2Fv1%2Fmessages%2Fbatches"
def _assert_input_is_encoded(url: str, prefix: str) -> None:
assert SAMPLE_INPUT_ENCODED in url
tail = url.split(prefix, 1)[1]
assert "../" not in tail, f"raw ``../`` after {prefix!r}: {tail!r}"
class TestEncodePathSegment:
def test_encodes_dot_slash_sequences(self):
assert encode_path_segment(SAMPLE_INPUT) == SAMPLE_INPUT_ENCODED
def test_encodes_query_and_fragment(self):
assert encode_path_segment("file?admin=1") == "file%3Fadmin%3D1"
assert encode_path_segment("file#frag") == "file%23frag"
def test_leaves_normal_ids_unchanged(self):
assert encode_path_segment("file-abc123") == "file-abc123"
assert encode_path_segment("file_abc123") == "file_abc123"
def test_rejects_bare_dotdot(self):
with pytest.raises(ValueError):
encode_path_segment("..")
with pytest.raises(ValueError):
encode_path_segment(".")
def test_rejects_none_and_empty(self):
with pytest.raises(ValueError, match="identifier is required"):
encode_path_segment(None)
with pytest.raises(ValueError, match="identifier is required"):
encode_path_segment("")
class TestEncodeUrlPath:
def test_preserves_legitimate_slashes_and_at(self):
assert (
encode_url_path("@cf/meta/llama-3.1-8b-instruct")
== "@cf/meta/llama-3.1-8b-instruct"
)
assert encode_url_path("google/gemma-3-4b-it") == "google/gemma-3-4b-it"
def test_rejects_dotdot_segment(self):
with pytest.raises(ValueError):
encode_url_path("../../etc/passwd")
with pytest.raises(ValueError):
encode_url_path("@cf/../secret")
def test_rejects_single_dot_segment(self):
with pytest.raises(ValueError):
encode_url_path("./secret")
def test_rejects_empty_segments(self):
with pytest.raises(ValueError):
encode_url_path("foo//bar")
with pytest.raises(ValueError):
encode_url_path("/foo")
with pytest.raises(ValueError):
encode_url_path("foo/")
def test_encodes_query_fragment_and_colon(self):
assert encode_url_path("model?x=1") == "model%3Fx%3D1"
assert encode_url_path("model#frag") == "model%23frag"
assert encode_url_path("evil.com:80/x") == "evil.com%3A80/x"
def test_none_becomes_empty(self):
assert encode_url_path(None) == ""
class TestAnthropicFilesEncoding:
def _config(self):
from litellm.llms.anthropic.files.transformation import AnthropicFilesConfig
return AnthropicFilesConfig()
def test_retrieve(self):
url, _ = self._config().transform_retrieve_file_request(
file_id=SAMPLE_INPUT,
optional_params={},
litellm_params={},
)
_assert_input_is_encoded(url, "/v1/files/")
def test_delete(self):
url, _ = self._config().transform_delete_file_request(
file_id=SAMPLE_INPUT,
optional_params={},
litellm_params={},
)
_assert_input_is_encoded(url, "/v1/files/")
def test_content(self):
url, _ = self._config().transform_file_content_request(
file_content_request={"file_id": SAMPLE_INPUT},
optional_params={},
litellm_params={},
)
_assert_input_is_encoded(url, "/v1/files/")
@pytest.mark.parametrize(
"method_name",
[
"transform_retrieve_file_request",
"transform_delete_file_request",
],
)
def test_missing_file_id_raises(self, method_name):
with pytest.raises(ValueError, match="identifier is required"):
getattr(self._config(), method_name)(
file_id="", optional_params={}, litellm_params={}
)
def test_missing_file_id_content_raises(self):
with pytest.raises(ValueError, match="identifier is required"):
self._config().transform_file_content_request(
file_content_request={}, # no file_id
optional_params={},
litellm_params={},
)
class TestAnthropicBatchesEncoding:
def test_retrieve_url(self):
from litellm.llms.anthropic.batches.transformation import AnthropicBatchesConfig
url = AnthropicBatchesConfig().get_retrieve_batch_url(
api_base="https://api.anthropic.com",
batch_id=SAMPLE_INPUT,
optional_params={},
litellm_params={},
)
_assert_input_is_encoded(url, "/batches/")
class TestAnthropicSkillsEncoding:
def test_get_complete_url(self):
from litellm.llms.anthropic.skills.transformation import AnthropicSkillsConfig
url = AnthropicSkillsConfig().get_complete_url(
api_base="https://api.anthropic.com",
endpoint="skills",
skill_id=SAMPLE_INPUT,
)
_assert_input_is_encoded(url, "/v1/skills/")
class TestOpenAIVideosEncoding:
def _config(self):
from litellm.llms.openai.videos.transformation import OpenAIVideoConfig
from litellm.types.router import GenericLiteLLMParams
return OpenAIVideoConfig(), GenericLiteLLMParams()
def test_content(self):
cfg, params = self._config()
url, _ = cfg.transform_video_content_request(
video_id=SAMPLE_INPUT,
api_base="https://api.openai.com/v1/videos",
litellm_params=params,
headers={},
)
_assert_input_is_encoded(url, "/v1/videos/")
def test_content_variant_is_encoded(self):
cfg, params = self._config()
url, _ = cfg.transform_video_content_request(
video_id="vid_ok",
api_base="https://api.openai.com/v1/videos",
litellm_params=params,
headers={},
variant="bad&inject=1",
)
assert "?variant=bad%26inject%3D1" in url
def test_delete(self):
cfg, params = self._config()
url, _ = cfg.transform_video_delete_request(
video_id=SAMPLE_INPUT,
api_base="https://api.openai.com/v1/videos",
litellm_params=params,
headers={},
)
_assert_input_is_encoded(url, "/v1/videos/")
def test_status_retrieve(self):
cfg, params = self._config()
url, _ = cfg.transform_video_status_retrieve_request(
video_id=SAMPLE_INPUT,
api_base="https://api.openai.com/v1/videos",
litellm_params=params,
headers={},
)
_assert_input_is_encoded(url, "/v1/videos/")
def test_remix(self):
cfg, params = self._config()
url, _ = cfg.transform_video_remix_request(
video_id=SAMPLE_INPUT,
prompt="x",
api_base="https://api.openai.com/v1/videos",
litellm_params=params,
headers={},
)
_assert_input_is_encoded(url, "/v1/videos/")
class TestOpenAIContainersEncoding:
def _config(self):
from litellm.llms.openai.containers.transformation import (
OpenAIContainerConfig,
)
from litellm.types.router import GenericLiteLLMParams
return OpenAIContainerConfig(), GenericLiteLLMParams()
def test_retrieve(self):
cfg, params = self._config()
url, _ = cfg.transform_container_retrieve_request(
container_id=SAMPLE_INPUT,
api_base="https://api.openai.com/v1/containers",
litellm_params=params,
headers={},
)
_assert_input_is_encoded(url, "/v1/containers/")
def test_delete(self):
cfg, params = self._config()
url, _ = cfg.transform_container_delete_request(
container_id=SAMPLE_INPUT,
api_base="https://api.openai.com/v1/containers",
litellm_params=params,
headers={},
)
_assert_input_is_encoded(url, "/v1/containers/")
def test_file_list(self):
cfg, params = self._config()
url, _ = cfg.transform_container_file_list_request(
container_id=SAMPLE_INPUT,
api_base="https://api.openai.com/v1/containers",
litellm_params=params,
headers={},
)
_assert_input_is_encoded(url, "/v1/containers/")
def test_file_content(self):
cfg, params = self._config()
url, _ = cfg.transform_container_file_content_request(
container_id="cntr_ok",
file_id=SAMPLE_INPUT,
api_base="https://api.openai.com/v1/containers",
litellm_params=params,
headers={},
)
_assert_input_is_encoded(url, "/cntr_ok/files/")
class TestOpenAIVectorStoresEncoding:
def test_search(self):
from litellm.llms.openai.vector_stores.transformation import (
OpenAIVectorStoreConfig,
)
url, _ = OpenAIVectorStoreConfig().transform_search_vector_store_request(
vector_store_id=SAMPLE_INPUT,
query="x",
vector_store_search_optional_params={},
api_base="https://api.openai.com/v1/vector_stores",
litellm_logging_obj=None,
litellm_params={},
)
_assert_input_is_encoded(url, "/v1/vector_stores/")
class TestOpenAIVectorStoreFilesEncoding:
def _config(self):
from litellm.llms.openai.vector_store_files.transformation import (
OpenAIVectorStoreFilesConfig,
)
return OpenAIVectorStoreFilesConfig()
def test_get_complete_url(self):
url = self._config().get_complete_url(
api_base="https://api.openai.com/v1",
vector_store_id=SAMPLE_INPUT,
litellm_params={},
)
_assert_input_is_encoded(url, "/vector_stores/")
def test_retrieve(self):
url, _ = self._config().transform_retrieve_vector_store_file_request(
vector_store_id="vs_ok",
file_id=SAMPLE_INPUT,
api_base="https://api.openai.com/v1/vector_stores/vs_ok/files",
)
_assert_input_is_encoded(url, "/files/")
def test_content(self):
url, _ = self._config().transform_retrieve_vector_store_file_content_request(
vector_store_id="vs_ok",
file_id=SAMPLE_INPUT,
api_base="https://api.openai.com/v1/vector_stores/vs_ok/files",
)
_assert_input_is_encoded(url, "/files/")
def test_update(self):
url, _ = self._config().transform_update_vector_store_file_request(
vector_store_id="vs_ok",
file_id=SAMPLE_INPUT,
update_request={"attributes": None},
api_base="https://api.openai.com/v1/vector_stores/vs_ok/files",
)
_assert_input_is_encoded(url, "/files/")
def test_delete(self):
url, _ = self._config().transform_delete_vector_store_file_request(
vector_store_id="vs_ok",
file_id=SAMPLE_INPUT,
api_base="https://api.openai.com/v1/vector_stores/vs_ok/files",
)
_assert_input_is_encoded(url, "/files/")
class TestGeminiInteractionsEncoding:
def _config(self):
from litellm.llms.gemini.interactions.transformation import (
GoogleAIStudioInteractionsConfig,
)
from litellm.types.router import GenericLiteLLMParams
return (
GoogleAIStudioInteractionsConfig(),
GenericLiteLLMParams(api_key="sk-test"),
)
def test_get(self):
cfg, params = self._config()
url, _ = cfg.transform_get_interaction_request(
interaction_id=SAMPLE_INPUT,
api_base="https://generativelanguage.googleapis.com",
litellm_params=params,
headers={},
)
_assert_input_is_encoded(url, "/interactions/")
def test_delete(self):
cfg, params = self._config()
url, _ = cfg.transform_delete_interaction_request(
interaction_id=SAMPLE_INPUT,
api_base="https://generativelanguage.googleapis.com",
litellm_params=params,
headers={},
)
_assert_input_is_encoded(url, "/interactions/")
def test_cancel(self):
cfg, params = self._config()
url, _ = cfg.transform_cancel_interaction_request(
interaction_id=SAMPLE_INPUT,
api_base="https://generativelanguage.googleapis.com",
litellm_params=params,
headers={},
)
_assert_input_is_encoded(url, "/interactions/")
class TestBedrockCountTokensEncoding:
def _config(self):
from litellm.llms.bedrock.count_tokens.transformation import (
BedrockCountTokensConfig,
)
return BedrockCountTokensConfig()
def test_endpoint(self):
url = self._config().get_bedrock_count_tokens_endpoint(
model=SAMPLE_INPUT,
aws_region_name="us-east-1",
api_base=None,
aws_bedrock_runtime_endpoint=None,
)
_assert_input_is_encoded(url, "/model/")
def test_versioned_model_id_preserves_colon(self):
url = self._config().get_bedrock_count_tokens_endpoint(
model="amazon.nova-pro-v1:0",
aws_region_name="us-east-1",
api_base=None,
aws_bedrock_runtime_endpoint=None,
)
assert "/model/amazon.nova-pro-v1:0/count-tokens" in url
class TestBedrockInvokeOpenAIEncoding:
def _config(self):
from litellm.llms.bedrock.chat.invoke_transformations.amazon_openai_transformation import (
AmazonBedrockOpenAIConfig,
)
return AmazonBedrockOpenAIConfig()
def test_versioned_model_id_preserves_colon(self):
cfg = self._config()
url = cfg.get_complete_url(
api_base=None,
api_key=None,
model="bedrock/openai/amazon.nova-pro-v1:0",
optional_params={"aws_region_name": "us-east-1"},
litellm_params={},
stream=False,
)
assert "/model/amazon.nova-pro-v1:0/invoke" in url
def test_traversal_input_is_encoded(self):
cfg = self._config()
url = cfg.get_complete_url(
api_base=None,
api_key=None,
model="bedrock/openai/../../foo",
optional_params={"aws_region_name": "us-east-1"},
litellm_params={},
stream=False,
)
assert "..%2F..%2Ffoo" in url
assert "/model/../" not in url
class TestCloudflareEncoding:
def test_rejects_dot_segment(self, monkeypatch):
from litellm.llms.cloudflare.chat.transformation import CloudflareChatConfig
monkeypatch.setenv("CLOUDFLARE_ACCOUNT_ID", "acct123")
cfg = CloudflareChatConfig()
with pytest.raises(ValueError):
cfg.get_complete_url(
api_base=None,
api_key="x",
model=SAMPLE_INPUT,
optional_params={},
litellm_params={},
stream=False,
)
def test_legitimate_model_preserved(self, monkeypatch):
from litellm.llms.cloudflare.chat.transformation import CloudflareChatConfig
monkeypatch.setenv("CLOUDFLARE_ACCOUNT_ID", "acct123")
cfg = CloudflareChatConfig()
url = cfg.get_complete_url(
api_base=None,
api_key="x",
model="@cf/meta/llama-3.1-8b-instruct",
optional_params={},
litellm_params={},
stream=False,
)
assert url.endswith("@cf/meta/llama-3.1-8b-instruct")
class TestBytezEncoding:
def test_rejects_dot_segment(self):
from litellm.llms.bytez.chat.transformation import BytezChatConfig
with pytest.raises(ValueError):
BytezChatConfig().get_complete_url(
api_base=None,
api_key="x",
model=SAMPLE_INPUT,
optional_params={},
litellm_params={},
stream=False,
)
def test_legitimate_model_preserved(self):
from litellm.llms.bytez.chat.transformation import BytezChatConfig
url = BytezChatConfig().get_complete_url(
api_base=None,
api_key="x",
model="google/gemma-3-4b-it",
optional_params={},
litellm_params={},
stream=False,
)
assert url.endswith("google/gemma-3-4b-it")
class TestRagflowEncoding:
def test_rejects_dot_segment(self):
from litellm.llms.ragflow.chat.transformation import RAGFlowConfig
cfg = RAGFlowConfig()
with pytest.raises(ValueError):
cfg.get_complete_url(
api_base="http://ragflow.example",
api_key="x",
model="ragflow/chat/../../v1/messages/batches/llama",
optional_params={},
litellm_params={},
stream=False,
)
def test_query_chars_in_entity_id_are_encoded(self):
from litellm.llms.ragflow.chat.transformation import RAGFlowConfig
cfg = RAGFlowConfig()
url = cfg.get_complete_url(
api_base="http://ragflow.example",
api_key="x",
model="ragflow/chat/id?admin=1/llama",
optional_params={},
litellm_params={},
stream=False,
)
assert "id%3Fadmin%3D1" in url
assert "?admin=1" not in url
class TestPGVectorEncoding:
def test_search(self):
from litellm.llms.pg_vector.vector_stores.transformation import (
PGVectorStoreConfig,
)
url, _ = PGVectorStoreConfig().transform_search_vector_store_request(
vector_store_id=SAMPLE_INPUT,
query="x",
vector_store_search_optional_params={},
api_base="http://pg.example/v1/vector_stores",
litellm_logging_obj=None,
litellm_params={},
)
_assert_input_is_encoded(url, "/v1/vector_stores/")
class TestContainerHandlerBuildUrl:
def test_path_params_encoded(self):
from litellm.llms.custom_httpx.container_handler import _build_url
url = _build_url(
api_base="https://api.openai.com/v1/containers",
path_template="/containers/{container_id}/files/{file_id}",
path_params={"container_id": "cntr_ok", "file_id": SAMPLE_INPUT},
)
_assert_input_is_encoded(url, "/files/")
class TestOpenAIEvalsEncoding:
def _config(self):
from litellm.llms.openai.evals.transformation import OpenAIEvalsConfig
return OpenAIEvalsConfig()
def test_get_complete_url(self):
cfg = self._config()
url = cfg.get_complete_url(
api_base="https://api.openai.com",
endpoint="evals",
eval_id=SAMPLE_INPUT,
)
_assert_input_is_encoded(url, "/v1/evals/")
def test_get_run(self):
from litellm.types.router import GenericLiteLLMParams
url, _ = self._config().transform_get_run_request(
eval_id="eval_ok",
run_id=SAMPLE_INPUT,
api_base="https://api.openai.com",
litellm_params=GenericLiteLLMParams(),
headers={},
)
_assert_input_is_encoded(url, "/runs/")
class TestOpenAIResponsesEncoding:
def _config(self):
from litellm.llms.openai.responses.transformation import (
OpenAIResponsesAPIConfig,
)
from litellm.types.router import GenericLiteLLMParams
return OpenAIResponsesAPIConfig(), GenericLiteLLMParams()
def test_delete(self):
cfg, params = self._config()
url, _ = cfg.transform_delete_response_api_request(
response_id=SAMPLE_INPUT,
api_base="https://api.openai.com/v1/responses",
litellm_params=params,
headers={},
)
_assert_input_is_encoded(url, "/v1/responses/")
class TestManusFilesEncoding:
def _config(self):
from litellm.llms.manus.files.transformation import ManusFilesConfig
return ManusFilesConfig()
def test_retrieve(self):
url, _ = self._config().transform_retrieve_file_request(
file_id=SAMPLE_INPUT,
optional_params={},
litellm_params={"api_base": "https://api.manus.im/v1/files"},
)
_assert_input_is_encoded(url, "/files/")
class TestAzureAIAgentsEncoding:
def _handler(self):
from litellm.llms.azure_ai.agents.handler import AzureAIAgentsHandler
return AzureAIAgentsHandler.__new__(AzureAIAgentsHandler)
def test_messages_url(self):
handler = self._handler()
url = handler._build_messages_url(
api_base="https://ai.example/api/projects/p",
thread_id=SAMPLE_INPUT,
api_version="2025-05-01",
)
_assert_input_is_encoded(url, "/threads/")
def test_run_status_url(self):
handler = self._handler()
url = handler._build_run_status_url(
api_base="https://ai.example/api/projects/p",
thread_id="thread_ok",
run_id=SAMPLE_INPUT,
api_version="2025-05-01",
)
_assert_input_is_encoded(url, "/runs/")
class TestVertexVideosEncoding:
def test_operation_status_rejects_dot_segments(self):
from litellm.llms.vertex_ai.videos.transformation import (
VertexAIVideoConfig,
)
from litellm.types.router import GenericLiteLLMParams
cfg = VertexAIVideoConfig()
with pytest.raises(ValueError):
cfg.transform_video_status_retrieve_request(
video_id="../../v1/models",
api_base="https://aiplatform.googleapis.com",
litellm_params=GenericLiteLLMParams(),
headers={},
)
class TestNvidiaNimEncoding:
def test_rejects_dot_segment(self):
from litellm.llms.nvidia_nim.rerank.transformation import (
NvidiaNimRerankConfig,
)
cfg = NvidiaNimRerankConfig()
with pytest.raises(ValueError):
cfg.get_complete_url(
api_base="https://integrate.api.nvidia.com/v1",
model="nvidia_nim/../../v1/models",
optional_params={},
)
def test_legitimate_model_preserved(self):
from litellm.llms.nvidia_nim.rerank.transformation import (
NvidiaNimRerankConfig,
)
url = NvidiaNimRerankConfig().get_complete_url(
api_base="https://integrate.api.nvidia.com",
model="nvidia_nim/nvidia/nv-rerankqa-mistral-4b-v3",
optional_params={},
)
assert url.endswith("nvidia/nv-rerankqa-mistral-4b-v3/reranking")
class TestHuggingfaceEmbeddingEncoding:
def test_ids_encoded(self):
assert encode_url_path("BAAI/bge-large-en-v1.5") == "BAAI/bge-large-en-v1.5"
class TestElevenLabsEncoding:
def test_voice_id_encoded(self):
from litellm.llms.elevenlabs.text_to_speech.transformation import (
ElevenLabsTextToSpeechConfig,
)
cfg = ElevenLabsTextToSpeechConfig()
url = cfg.get_complete_url(
model="eleven_multilingual_v2",
api_base="https://api.elevenlabs.io",
litellm_params={
cfg.ELEVENLABS_VOICE_ID_KEY: SAMPLE_INPUT,
},
)
assert SAMPLE_INPUT_ENCODED in url
assert "../" not in url.split("/text-to-speech/", 1)[1]