fix: encode upstream URL path identifiers
This commit is contained in:
parent
d3891e6eae
commit
d4dd865b1a
@ -22,7 +22,7 @@ Admins can opt out via two ``litellm`` globals (wired from proxy config):
|
||||
import socket
|
||||
from ipaddress import ip_address, ip_network
|
||||
from typing import Any, List, Set, Tuple
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
from urllib.parse import quote, urlparse, urlunparse
|
||||
|
||||
import httpx
|
||||
|
||||
@ -46,6 +46,26 @@ class SSRFError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def encode_url_path_segment(value: Any, *, field_name: str = "path parameter") -> str:
|
||||
"""Percent-encode one user-controlled URL path segment.
|
||||
|
||||
``urllib.parse.quote(..., safe="")`` intentionally leaves RFC 3986
|
||||
unreserved characters such as ``.`` unescaped, so reject standalone dot
|
||||
segments before they can be appended to an upstream URL and normalized by
|
||||
the HTTP client.
|
||||
"""
|
||||
if value is None:
|
||||
raise ValueError(f"{field_name} is required")
|
||||
|
||||
value_str = str(value)
|
||||
if value_str == "":
|
||||
raise ValueError(f"{field_name} is required")
|
||||
if value_str in {".", ".."}:
|
||||
raise ValueError(f"{field_name} cannot be a dot path segment")
|
||||
|
||||
return quote(value_str, safe="")
|
||||
|
||||
|
||||
def _is_blocked_ip(addr: str) -> bool:
|
||||
"""Return True for any IP not safe to reach from a user-supplied URL.
|
||||
|
||||
|
||||
@ -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.litellm_core_utils.url_utils import encode_url_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,8 @@ 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}"
|
||||
encoded_batch_id = encode_url_path_segment(batch_id, field_name="batch_id")
|
||||
return f"{api_base.rstrip('/')}/v1/messages/batches/{encoded_batch_id}"
|
||||
|
||||
def transform_retrieve_batch_request(
|
||||
self,
|
||||
|
||||
@ -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.litellm_core_utils.url_utils import encode_url_path_segment
|
||||
from litellm.llms.custom_httpx.http_handler import get_async_httpx_client
|
||||
from litellm.types.llms.openai import (
|
||||
FileContentRequest,
|
||||
@ -89,7 +90,10 @@ 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"
|
||||
encoded_batch_id = encode_url_path_segment(batch_id, field_name="batch_id")
|
||||
results_url = (
|
||||
f"{api_base.rstrip('/')}/v1/messages/batches/{encoded_batch_id}/results"
|
||||
)
|
||||
|
||||
# Prepare headers
|
||||
headers = {
|
||||
|
||||
@ -19,6 +19,7 @@ from typing import Any, Dict, List, Optional, Union, cast
|
||||
import httpx
|
||||
from openai.types.file_deleted import FileDeleted
|
||||
|
||||
from litellm.litellm_core_utils.url_utils import encode_url_path_segment
|
||||
from litellm.litellm_core_utils.prompt_templates.common_utils import extract_file_data
|
||||
from litellm.llms.base_llm.chat.transformation import BaseLLMException
|
||||
from litellm.llms.base_llm.files.transformation import (
|
||||
@ -185,7 +186,8 @@ 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}", {}
|
||||
encoded_file_id = encode_url_path_segment(file_id, field_name="file_id")
|
||||
return f"{api_base.rstrip('/')}/v1/files/{encoded_file_id}", {}
|
||||
|
||||
def transform_retrieve_file_response(
|
||||
self,
|
||||
@ -206,7 +208,8 @@ 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}", {}
|
||||
encoded_file_id = encode_url_path_segment(file_id, field_name="file_id")
|
||||
return f"{api_base.rstrip('/')}/v1/files/{encoded_file_id}", {}
|
||||
|
||||
def transform_delete_file_response(
|
||||
self,
|
||||
@ -268,7 +271,8 @@ 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", {}
|
||||
encoded_file_id = encode_url_path_segment(file_id, field_name="file_id")
|
||||
return f"{api_base.rstrip('/')}/v1/files/{encoded_file_id}/content", {}
|
||||
|
||||
def transform_file_content_response(
|
||||
self,
|
||||
|
||||
@ -7,6 +7,7 @@ from typing import Any, Dict, Optional, Tuple
|
||||
import httpx
|
||||
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.litellm_core_utils.url_utils import encode_url_path_segment
|
||||
from litellm.llms.base_llm.skills.transformation import (
|
||||
BaseSkillsAPIConfig,
|
||||
LiteLLMLoggingObj,
|
||||
@ -81,7 +82,8 @@ class AnthropicSkillsConfig(BaseSkillsAPIConfig):
|
||||
api_base = AnthropicModelInfo.get_api_base()
|
||||
|
||||
if skill_id:
|
||||
return f"{api_base}/v1/skills/{skill_id}"
|
||||
encoded_skill_id = encode_url_path_segment(skill_id, field_name="skill_id")
|
||||
return f"{api_base}/v1/skills/{encoded_skill_id}"
|
||||
return f"{api_base}/v1/{endpoint}"
|
||||
|
||||
def transform_create_skill_request(
|
||||
|
||||
@ -5,6 +5,7 @@ import httpx
|
||||
from openai.types.responses import ResponseReasoningItem
|
||||
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.litellm_core_utils.url_utils import encode_url_path_segment
|
||||
from litellm.llms.azure.common_utils import BaseAzureLLM
|
||||
from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig
|
||||
from litellm.types.llms.openai import *
|
||||
@ -201,7 +202,10 @@ 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}"
|
||||
encoded_response_id = encode_url_path_segment(
|
||||
response_id, field_name="response_id"
|
||||
)
|
||||
new_path = f"{path}/{encoded_response_id}"
|
||||
|
||||
# Reconstruct the URL with all original components but with the modified path
|
||||
constructed_url = urlunparse(
|
||||
@ -322,7 +326,10 @@ 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"
|
||||
encoded_response_id = encode_url_path_segment(
|
||||
response_id, field_name="response_id"
|
||||
)
|
||||
new_path = f"{path}/{encoded_response_id}/cancel"
|
||||
|
||||
# Reconstruct the URL with all original components but with the modified path
|
||||
cancel_url = urlunparse(
|
||||
|
||||
@ -201,13 +201,14 @@ class BedrockCountTokensConfig(BaseAWSLLM):
|
||||
# Remove bedrock/ prefix if present
|
||||
if model_id.startswith("bedrock/"):
|
||||
model_id = model_id[8:] # Remove "bedrock/" prefix
|
||||
encoded_model_id = self.encode_model_id(model_id=model_id)
|
||||
|
||||
base_url, _ = self.get_runtime_endpoint(
|
||||
api_base=api_base,
|
||||
aws_bedrock_runtime_endpoint=aws_bedrock_runtime_endpoint,
|
||||
aws_region_name=aws_region_name,
|
||||
)
|
||||
endpoint = f"{base_url}/model/{model_id}/count-tokens"
|
||||
endpoint = f"{base_url}/model/{encoded_model_id}/count-tokens"
|
||||
|
||||
return endpoint
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ from urllib.parse import urlparse
|
||||
import httpx
|
||||
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.litellm_core_utils.url_utils import encode_url_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 (
|
||||
@ -209,7 +210,10 @@ class BedrockVectorStoreConfig(BaseVectorStoreConfig, BaseAWSLLM):
|
||||
if isinstance(query, list):
|
||||
query = " ".join(query)
|
||||
|
||||
url = f"{api_base}/{vector_store_id}/retrieve"
|
||||
encoded_vector_store_id = encode_url_path_segment(
|
||||
vector_store_id, field_name="vector_store_id"
|
||||
)
|
||||
url = f"{api_base}/{encoded_vector_store_id}/retrieve"
|
||||
|
||||
request_body: Dict[str, Any] = {
|
||||
"retrievalQuery": BedrockKBRetrievalQuery(text=query),
|
||||
|
||||
@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
|
||||
|
||||
import httpx
|
||||
|
||||
from litellm.litellm_core_utils.url_utils import encode_url_path_segment
|
||||
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.chat.transformation import BaseConfig, BaseLLMException
|
||||
@ -149,7 +150,8 @@ class BytezChatConfig(BaseConfig):
|
||||
litellm_params: dict,
|
||||
stream: Optional[bool] = None,
|
||||
) -> str:
|
||||
return f"{API_BASE}/{model}"
|
||||
encoded_model = encode_url_path_segment(model, field_name="model")
|
||||
return f"{API_BASE}/{encoded_model}"
|
||||
|
||||
def transform_request(
|
||||
self,
|
||||
|
||||
@ -5,6 +5,7 @@ from typing import AsyncIterator, Iterator, List, Optional, Union
|
||||
import httpx
|
||||
|
||||
import litellm
|
||||
from litellm.litellm_core_utils.url_utils import encode_url_path_segment
|
||||
from litellm.llms.base_llm.base_model_iterator import BaseModelResponseIterator
|
||||
from litellm.llms.base_llm.chat.transformation import (
|
||||
BaseConfig,
|
||||
@ -89,7 +90,8 @@ class CloudflareChatConfig(BaseConfig):
|
||||
api_base = (
|
||||
f"https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/run/"
|
||||
)
|
||||
return api_base + model
|
||||
encoded_model = encode_url_path_segment(model, field_name="model")
|
||||
return api_base + encoded_model
|
||||
|
||||
def get_supported_openai_params(self, model: str) -> List[str]:
|
||||
return [
|
||||
|
||||
@ -12,6 +12,7 @@ from typing import TYPE_CHECKING, Any, Coroutine, Dict, Optional, Type, Union
|
||||
import httpx
|
||||
|
||||
import litellm
|
||||
from litellm.litellm_core_utils.url_utils import encode_url_path_segment
|
||||
from litellm.llms.custom_httpx.http_handler import (
|
||||
AsyncHTTPHandler,
|
||||
HTTPHandler,
|
||||
@ -72,7 +73,8 @@ def _build_url(
|
||||
|
||||
# Substitute path parameters
|
||||
for param, value in path_params.items():
|
||||
path_template = path_template.replace(f"{{{param}}}", value)
|
||||
encoded_value = encode_url_path_segment(value, field_name=param)
|
||||
path_template = path_template.replace(f"{{{param}}}", encoded_value)
|
||||
|
||||
# Parse the api_base to extract existing query params
|
||||
parsed_base = httpx.URL(api_base)
|
||||
|
||||
@ -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.litellm_core_utils.url_utils import encode_url_path_segment
|
||||
from litellm.llms.base_llm.anthropic_messages.transformation import (
|
||||
BaseAnthropicMessagesConfig,
|
||||
)
|
||||
@ -8934,7 +8935,10 @@ class BaseLLMHTTPHandler:
|
||||
litellm_params=dict(litellm_params),
|
||||
)
|
||||
|
||||
url = f"{api_base}/{vector_store_id}"
|
||||
encoded_vector_store_id = encode_url_path_segment(
|
||||
vector_store_id, field_name="vector_store_id"
|
||||
)
|
||||
url = f"{api_base}/{encoded_vector_store_id}"
|
||||
|
||||
logging_obj.pre_call(
|
||||
input="",
|
||||
@ -9001,7 +9005,10 @@ class BaseLLMHTTPHandler:
|
||||
litellm_params=dict(litellm_params),
|
||||
)
|
||||
|
||||
url = f"{api_base}/{vector_store_id}"
|
||||
encoded_vector_store_id = encode_url_path_segment(
|
||||
vector_store_id, field_name="vector_store_id"
|
||||
)
|
||||
url = f"{api_base}/{encoded_vector_store_id}"
|
||||
|
||||
logging_obj.pre_call(
|
||||
input="",
|
||||
@ -9200,7 +9207,10 @@ class BaseLLMHTTPHandler:
|
||||
litellm_params=dict(litellm_params),
|
||||
)
|
||||
|
||||
url = f"{api_base}/{vector_store_id}"
|
||||
encoded_vector_store_id = encode_url_path_segment(
|
||||
vector_store_id, field_name="vector_store_id"
|
||||
)
|
||||
url = f"{api_base}/{encoded_vector_store_id}"
|
||||
|
||||
request_body: Dict[str, Any] = dict(vector_store_update_optional_params)
|
||||
|
||||
@ -9283,7 +9293,10 @@ class BaseLLMHTTPHandler:
|
||||
litellm_params=dict(litellm_params),
|
||||
)
|
||||
|
||||
url = f"{api_base}/{vector_store_id}"
|
||||
encoded_vector_store_id = encode_url_path_segment(
|
||||
vector_store_id, field_name="vector_store_id"
|
||||
)
|
||||
url = f"{api_base}/{encoded_vector_store_id}"
|
||||
|
||||
request_body: Dict[str, Any] = dict(vector_store_update_optional_params)
|
||||
|
||||
@ -9349,7 +9362,10 @@ class BaseLLMHTTPHandler:
|
||||
litellm_params=dict(litellm_params),
|
||||
)
|
||||
|
||||
url = f"{api_base}/{vector_store_id}"
|
||||
encoded_vector_store_id = encode_url_path_segment(
|
||||
vector_store_id, field_name="vector_store_id"
|
||||
)
|
||||
url = f"{api_base}/{encoded_vector_store_id}"
|
||||
|
||||
logging_obj.pre_call(
|
||||
input="",
|
||||
@ -9414,7 +9430,10 @@ class BaseLLMHTTPHandler:
|
||||
litellm_params=dict(litellm_params),
|
||||
)
|
||||
|
||||
url = f"{api_base}/{vector_store_id}"
|
||||
encoded_vector_store_id = encode_url_path_segment(
|
||||
vector_store_id, field_name="vector_store_id"
|
||||
)
|
||||
url = f"{api_base}/{encoded_vector_store_id}"
|
||||
|
||||
logging_obj.pre_call(
|
||||
input="",
|
||||
|
||||
@ -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.litellm_core_utils.url_utils import encode_url_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 (
|
||||
@ -205,8 +206,11 @@ class GoogleAIStudioInteractionsConfig(BaseInteractionsAPIConfig):
|
||||
resolved_api_base = GeminiModelInfo.get_api_base(api_base)
|
||||
if not GeminiModelInfo.get_api_key(litellm_params.api_key):
|
||||
raise ValueError("Google API key is required")
|
||||
encoded_interaction_id = encode_url_path_segment(
|
||||
interaction_id, field_name="interaction_id"
|
||||
)
|
||||
return (
|
||||
f"{resolved_api_base}/{self.api_version}/interactions/{interaction_id}",
|
||||
f"{resolved_api_base}/{self.api_version}/interactions/{encoded_interaction_id}",
|
||||
{},
|
||||
)
|
||||
|
||||
@ -238,8 +242,11 @@ class GoogleAIStudioInteractionsConfig(BaseInteractionsAPIConfig):
|
||||
resolved_api_base = GeminiModelInfo.get_api_base(api_base)
|
||||
if not GeminiModelInfo.get_api_key(litellm_params.api_key):
|
||||
raise ValueError("Google API key is required")
|
||||
encoded_interaction_id = encode_url_path_segment(
|
||||
interaction_id, field_name="interaction_id"
|
||||
)
|
||||
return (
|
||||
f"{resolved_api_base}/{self.api_version}/interactions/{interaction_id}",
|
||||
f"{resolved_api_base}/{self.api_version}/interactions/{encoded_interaction_id}",
|
||||
{},
|
||||
)
|
||||
|
||||
@ -268,8 +275,11 @@ class GoogleAIStudioInteractionsConfig(BaseInteractionsAPIConfig):
|
||||
resolved_api_base = GeminiModelInfo.get_api_base(api_base)
|
||||
if not GeminiModelInfo.get_api_key(litellm_params.api_key):
|
||||
raise ValueError("Google API key is required")
|
||||
encoded_interaction_id = encode_url_path_segment(
|
||||
interaction_id, field_name="interaction_id"
|
||||
)
|
||||
return (
|
||||
f"{resolved_api_base}/{self.api_version}/interactions/{interaction_id}:cancel",
|
||||
f"{resolved_api_base}/{self.api_version}/interactions/{encoded_interaction_id}:cancel",
|
||||
{},
|
||||
)
|
||||
|
||||
|
||||
@ -18,6 +18,7 @@ from openai.types.file_deleted import FileDeleted
|
||||
|
||||
import litellm
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.litellm_core_utils.url_utils import encode_url_path_segment
|
||||
from litellm.litellm_core_utils.prompt_templates.common_utils import extract_file_data
|
||||
from litellm.llms.base_llm.chat.transformation import BaseLLMException
|
||||
from litellm.llms.base_llm.files.transformation import (
|
||||
@ -306,7 +307,8 @@ class ManusFilesConfig(BaseFilesConfig):
|
||||
optional_params=optional_params,
|
||||
litellm_params=litellm_params,
|
||||
)
|
||||
return f"{api_base}/{file_id}", {}
|
||||
encoded_file_id = encode_url_path_segment(file_id, field_name="file_id")
|
||||
return f"{api_base}/{encoded_file_id}", {}
|
||||
|
||||
def transform_retrieve_file_response(
|
||||
self,
|
||||
@ -336,7 +338,8 @@ class ManusFilesConfig(BaseFilesConfig):
|
||||
optional_params=optional_params,
|
||||
litellm_params=litellm_params,
|
||||
)
|
||||
return f"{api_base}/{file_id}", {}
|
||||
encoded_file_id = encode_url_path_segment(file_id, field_name="file_id")
|
||||
return f"{api_base}/{encoded_file_id}", {}
|
||||
|
||||
def transform_delete_file_response(
|
||||
self,
|
||||
@ -422,7 +425,8 @@ class ManusFilesConfig(BaseFilesConfig):
|
||||
optional_params=optional_params,
|
||||
litellm_params=litellm_params,
|
||||
)
|
||||
return f"{api_base}/{file_id}/content", {}
|
||||
encoded_file_id = encode_url_path_segment(file_id, field_name="file_id")
|
||||
return f"{api_base}/{encoded_file_id}/content", {}
|
||||
|
||||
def transform_file_content_response(
|
||||
self,
|
||||
|
||||
@ -6,6 +6,7 @@ import httpx
|
||||
import litellm
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.litellm_core_utils.core_helpers import process_response_headers
|
||||
from litellm.litellm_core_utils.url_utils import encode_url_path_segment
|
||||
from litellm.litellm_core_utils.llm_response_utils.convert_dict_to_response import (
|
||||
_safe_convert_created_field,
|
||||
)
|
||||
@ -270,7 +271,10 @@ class ManusResponsesAPIConfig(OpenAIResponsesAPIConfig):
|
||||
|
||||
Reference: https://open.manus.im/docs/openai-compatibility
|
||||
"""
|
||||
url = f"{api_base}/{response_id}"
|
||||
encoded_response_id = encode_url_path_segment(
|
||||
response_id, field_name="response_id"
|
||||
)
|
||||
url = f"{api_base}/{encoded_response_id}"
|
||||
data: Dict = {}
|
||||
return url, data
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ import litellm
|
||||
from litellm.litellm_core_utils.llm_cost_calc.tool_call_cost_tracking import (
|
||||
StandardBuiltInToolCostTracking,
|
||||
)
|
||||
from litellm.litellm_core_utils.url_utils import encode_url_path_segment
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
from litellm.types.containers.main import (
|
||||
ContainerCreateOptionalRequestParams,
|
||||
@ -198,7 +199,10 @@ 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}")
|
||||
encoded_container_id = encode_url_path_segment(
|
||||
container_id, field_name="container_id"
|
||||
)
|
||||
url = join_container_api_base_path(api_base, f"/{encoded_container_id}")
|
||||
|
||||
# No additional data needed for GET request
|
||||
data: Dict[str, Any] = {}
|
||||
@ -230,7 +234,10 @@ 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}")
|
||||
encoded_container_id = encode_url_path_segment(
|
||||
container_id, field_name="container_id"
|
||||
)
|
||||
url = join_container_api_base_path(api_base, f"/{encoded_container_id}")
|
||||
|
||||
# No data needed for DELETE request
|
||||
data: Dict[str, Any] = {}
|
||||
@ -267,7 +274,10 @@ 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")
|
||||
encoded_container_id = encode_url_path_segment(
|
||||
container_id, field_name="container_id"
|
||||
)
|
||||
url = join_container_api_base_path(api_base, f"/{encoded_container_id}/files")
|
||||
|
||||
# Prepare query parameters
|
||||
params: Dict[str, Any] = {}
|
||||
@ -311,8 +321,12 @@ class OpenAIContainerConfig(BaseContainerConfig):
|
||||
- GET /v1/containers/{container_id}/files/{file_id}/content
|
||||
"""
|
||||
# Construct the URL for container file content
|
||||
encoded_container_id = encode_url_path_segment(
|
||||
container_id, field_name="container_id"
|
||||
)
|
||||
encoded_file_id = encode_url_path_segment(file_id, field_name="file_id")
|
||||
url = join_container_api_base_path(
|
||||
api_base, f"/{container_id}/files/{file_id}/content"
|
||||
api_base, f"/{encoded_container_id}/files/{encoded_file_id}/content"
|
||||
)
|
||||
|
||||
# No query parameters needed
|
||||
|
||||
@ -7,6 +7,7 @@ from typing import Any, Dict, Optional, Tuple
|
||||
import httpx
|
||||
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.litellm_core_utils.url_utils import encode_url_path_segment
|
||||
from litellm.llms.base_llm.evals.transformation import (
|
||||
BaseEvalsAPIConfig,
|
||||
LiteLLMLoggingObj,
|
||||
@ -76,7 +77,8 @@ class OpenAIEvalsConfig(BaseEvalsAPIConfig):
|
||||
api_base = "https://api.openai.com"
|
||||
|
||||
if eval_id:
|
||||
return f"{api_base}/v1/evals/{eval_id}"
|
||||
encoded_eval_id = encode_url_path_segment(eval_id, field_name="eval_id")
|
||||
return f"{api_base}/v1/evals/{encoded_eval_id}"
|
||||
return f"{api_base}/v1/{endpoint}"
|
||||
|
||||
def transform_create_eval_request(
|
||||
@ -276,7 +278,8 @@ 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"
|
||||
encoded_eval_id = encode_url_path_segment(eval_id, field_name="eval_id")
|
||||
url = f"{api_base}/v1/evals/{encoded_eval_id}/runs"
|
||||
|
||||
# Build request body
|
||||
request_body = {k: v for k, v in create_request.items() if v is not None}
|
||||
@ -310,7 +313,8 @@ 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"
|
||||
encoded_eval_id = encode_url_path_segment(eval_id, field_name="eval_id")
|
||||
url = f"{api_base}/v1/evals/{encoded_eval_id}/runs"
|
||||
|
||||
# Build query parameters
|
||||
query_params: Dict[str, Any] = {}
|
||||
@ -350,7 +354,9 @@ 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}"
|
||||
encoded_eval_id = encode_url_path_segment(eval_id, field_name="eval_id")
|
||||
encoded_run_id = encode_url_path_segment(run_id, field_name="run_id")
|
||||
url = f"{api_base}/v1/evals/{encoded_eval_id}/runs/{encoded_run_id}"
|
||||
|
||||
verbose_logger.debug("Get run request - URL: %s", url)
|
||||
|
||||
@ -376,7 +382,9 @@ 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"
|
||||
encoded_eval_id = encode_url_path_segment(eval_id, field_name="eval_id")
|
||||
encoded_run_id = encode_url_path_segment(run_id, field_name="run_id")
|
||||
url = f"{api_base}/v1/evals/{encoded_eval_id}/runs/{encoded_run_id}/cancel"
|
||||
|
||||
# Empty body for cancel request
|
||||
request_body: Dict[str, Any] = {}
|
||||
@ -405,7 +413,9 @@ 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}"
|
||||
encoded_eval_id = encode_url_path_segment(eval_id, field_name="eval_id")
|
||||
encoded_run_id = encode_url_path_segment(run_id, field_name="run_id")
|
||||
url = f"{api_base}/v1/evals/{encoded_eval_id}/runs/{encoded_run_id}"
|
||||
|
||||
# Empty body for delete request
|
||||
request_body: Dict[str, Any] = {}
|
||||
|
||||
@ -7,6 +7,7 @@ from pydantic import BaseModel, ValidationError
|
||||
import litellm
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.litellm_core_utils.core_helpers import process_response_headers
|
||||
from litellm.litellm_core_utils.url_utils import encode_url_path_segment
|
||||
from litellm.litellm_core_utils.llm_response_utils.convert_dict_to_response import (
|
||||
_safe_convert_created_field,
|
||||
)
|
||||
@ -421,7 +422,10 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig):
|
||||
OpenAI API expects the following request
|
||||
- DELETE /v1/responses/{response_id}
|
||||
"""
|
||||
url = f"{api_base}/{response_id}"
|
||||
encoded_response_id = encode_url_path_segment(
|
||||
response_id, field_name="response_id"
|
||||
)
|
||||
url = f"{api_base}/{encoded_response_id}"
|
||||
data: Dict = {}
|
||||
return url, data
|
||||
|
||||
@ -457,7 +461,10 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig):
|
||||
OpenAI API expects the following request
|
||||
- GET /v1/responses/{response_id}
|
||||
"""
|
||||
url = f"{api_base}/{response_id}"
|
||||
encoded_response_id = encode_url_path_segment(
|
||||
response_id, field_name="response_id"
|
||||
)
|
||||
url = f"{api_base}/{encoded_response_id}"
|
||||
data: Dict = {}
|
||||
return url, data
|
||||
|
||||
@ -498,7 +505,10 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig):
|
||||
limit: int = 20,
|
||||
order: Literal["asc", "desc"] = "desc",
|
||||
) -> Tuple[str, Dict]:
|
||||
url = f"{api_base}/{response_id}/input_items"
|
||||
encoded_response_id = encode_url_path_segment(
|
||||
response_id, field_name="response_id"
|
||||
)
|
||||
url = f"{api_base}/{encoded_response_id}/input_items"
|
||||
params: Dict[str, Any] = {}
|
||||
if after is not None:
|
||||
params["after"] = after
|
||||
@ -540,7 +550,10 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig):
|
||||
OpenAI API expects the following request
|
||||
- POST /v1/responses/{response_id}/cancel
|
||||
"""
|
||||
url = f"{api_base}/{response_id}/cancel"
|
||||
encoded_response_id = encode_url_path_segment(
|
||||
response_id, field_name="response_id"
|
||||
)
|
||||
url = f"{api_base}/{encoded_response_id}/cancel"
|
||||
data: Dict = {}
|
||||
return url, data
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ from typing import Any, Dict, Optional, Tuple, cast
|
||||
import httpx
|
||||
|
||||
import litellm
|
||||
from litellm.litellm_core_utils.url_utils import encode_url_path_segment
|
||||
from litellm.llms.base_llm.vector_store_files.transformation import (
|
||||
BaseVectorStoreFilesConfig,
|
||||
)
|
||||
@ -98,7 +99,10 @@ class OpenAIVectorStoreFilesConfig(BaseVectorStoreFilesConfig):
|
||||
or "https://api.openai.com/v1"
|
||||
)
|
||||
base_url = base_url.rstrip("/")
|
||||
return f"{base_url}/vector_stores/{vector_store_id}/files"
|
||||
encoded_vector_store_id = encode_url_path_segment(
|
||||
vector_store_id, field_name="vector_store_id"
|
||||
)
|
||||
return f"{base_url}/vector_stores/{encoded_vector_store_id}/files"
|
||||
|
||||
def transform_create_vector_store_file_request(
|
||||
self,
|
||||
@ -163,7 +167,8 @@ class OpenAIVectorStoreFilesConfig(BaseVectorStoreFilesConfig):
|
||||
file_id: str,
|
||||
api_base: str,
|
||||
) -> Tuple[str, Dict[str, Any]]:
|
||||
return f"{api_base}/{file_id}", {}
|
||||
encoded_file_id = encode_url_path_segment(file_id, field_name="file_id")
|
||||
return f"{api_base}/{encoded_file_id}", {}
|
||||
|
||||
def transform_retrieve_vector_store_file_response(
|
||||
self,
|
||||
@ -186,7 +191,8 @@ class OpenAIVectorStoreFilesConfig(BaseVectorStoreFilesConfig):
|
||||
file_id: str,
|
||||
api_base: str,
|
||||
) -> Tuple[str, Dict[str, Any]]:
|
||||
return f"{api_base}/{file_id}/content", {}
|
||||
encoded_file_id = encode_url_path_segment(file_id, field_name="file_id")
|
||||
return f"{api_base}/{encoded_file_id}/content", {}
|
||||
|
||||
def transform_retrieve_vector_store_file_content_response(
|
||||
self,
|
||||
@ -218,7 +224,8 @@ class OpenAIVectorStoreFilesConfig(BaseVectorStoreFilesConfig):
|
||||
payload["attributes"] = filtered_attributes
|
||||
else:
|
||||
payload.pop("attributes", None)
|
||||
return f"{api_base}/{file_id}", payload
|
||||
encoded_file_id = encode_url_path_segment(file_id, field_name="file_id")
|
||||
return f"{api_base}/{encoded_file_id}", payload
|
||||
|
||||
def transform_update_vector_store_file_response(
|
||||
self,
|
||||
@ -241,7 +248,8 @@ class OpenAIVectorStoreFilesConfig(BaseVectorStoreFilesConfig):
|
||||
file_id: str,
|
||||
api_base: str,
|
||||
) -> Tuple[str, Dict[str, Any]]:
|
||||
return f"{api_base}/{file_id}", {}
|
||||
encoded_file_id = encode_url_path_segment(file_id, field_name="file_id")
|
||||
return f"{api_base}/{encoded_file_id}", {}
|
||||
|
||||
def transform_delete_vector_store_file_response(
|
||||
self,
|
||||
|
||||
@ -3,6 +3,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, cast
|
||||
import httpx
|
||||
|
||||
import litellm
|
||||
from litellm.litellm_core_utils.url_utils import encode_url_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
|
||||
@ -108,7 +109,10 @@ class OpenAIVectorStoreConfig(BaseVectorStoreConfig):
|
||||
litellm_params: dict,
|
||||
extra_body: Optional[Dict[str, Any]] = None,
|
||||
) -> Tuple[str, Dict]:
|
||||
url = f"{api_base}/{vector_store_id}/search"
|
||||
encoded_vector_store_id = encode_url_path_segment(
|
||||
vector_store_id, field_name="vector_store_id"
|
||||
)
|
||||
url = f"{api_base}/{encoded_vector_store_id}/search"
|
||||
typed_request_body = VectorStoreSearchRequest(
|
||||
query=query,
|
||||
filters=vector_store_search_optional_params.get("filters", None),
|
||||
|
||||
@ -6,6 +6,7 @@ import httpx
|
||||
from httpx._types import RequestFiles
|
||||
|
||||
import litellm
|
||||
from litellm.litellm_core_utils.url_utils import encode_url_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
|
||||
@ -220,9 +221,12 @@ class OpenAIVideoConfig(BaseVideoConfig):
|
||||
- GET /v1/videos/{video_id}/content?variant=thumbnail
|
||||
"""
|
||||
original_video_id = extract_original_video_id(video_id)
|
||||
encoded_video_id = encode_url_path_segment(
|
||||
original_video_id, field_name="video_id"
|
||||
)
|
||||
|
||||
# Construct the URL for video content download
|
||||
url = f"{api_base.rstrip('/')}/{original_video_id}/content"
|
||||
url = f"{api_base.rstrip('/')}/{encoded_video_id}/content"
|
||||
if variant is not None:
|
||||
url = f"{url}?variant={variant}"
|
||||
|
||||
@ -247,9 +251,12 @@ class OpenAIVideoConfig(BaseVideoConfig):
|
||||
- POST /v1/videos/{video_id}/remix
|
||||
"""
|
||||
original_video_id = extract_original_video_id(video_id)
|
||||
encoded_video_id = encode_url_path_segment(
|
||||
original_video_id, field_name="video_id"
|
||||
)
|
||||
|
||||
# Construct the URL for video remix
|
||||
url = f"{api_base.rstrip('/')}/{original_video_id}/remix"
|
||||
url = f"{api_base.rstrip('/')}/{encoded_video_id}/remix"
|
||||
|
||||
# Prepare the request data
|
||||
data = {"prompt": prompt}
|
||||
@ -391,9 +398,12 @@ class OpenAIVideoConfig(BaseVideoConfig):
|
||||
- DELETE /v1/videos/{video_id}
|
||||
"""
|
||||
original_video_id = extract_original_video_id(video_id)
|
||||
encoded_video_id = encode_url_path_segment(
|
||||
original_video_id, field_name="video_id"
|
||||
)
|
||||
|
||||
# Construct the URL for video delete
|
||||
url = f"{api_base.rstrip('/')}/{original_video_id}"
|
||||
url = f"{api_base.rstrip('/')}/{encoded_video_id}"
|
||||
|
||||
# No data needed for DELETE request
|
||||
data: Dict[str, Any] = {}
|
||||
@ -427,9 +437,12 @@ class OpenAIVideoConfig(BaseVideoConfig):
|
||||
"""
|
||||
# Extract the original video_id (remove provider encoding if present)
|
||||
original_video_id = extract_original_video_id(video_id)
|
||||
encoded_video_id = encode_url_path_segment(
|
||||
original_video_id, field_name="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('/')}/{encoded_video_id}"
|
||||
|
||||
# No additional data needed for GET request
|
||||
data: Dict[str, Any] = {}
|
||||
@ -494,7 +507,11 @@ class OpenAIVideoConfig(BaseVideoConfig):
|
||||
litellm_params: GenericLiteLLMParams,
|
||||
headers: dict,
|
||||
) -> Tuple[str, Dict]:
|
||||
url = f"{api_base.rstrip('/')}/characters/{character_id}"
|
||||
original_character_id = extract_original_character_id(character_id)
|
||||
encoded_character_id = encode_url_path_segment(
|
||||
original_character_id, field_name="character_id"
|
||||
)
|
||||
url = f"{api_base.rstrip('/')}/characters/{encoded_character_id}"
|
||||
return url, {}
|
||||
|
||||
def transform_video_get_character_response(
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
from litellm.litellm_core_utils.url_utils import encode_url_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
|
||||
@ -82,7 +83,10 @@ class PGVectorStoreConfig(OpenAIVectorStoreConfig):
|
||||
litellm_params: dict,
|
||||
extra_body: Optional[Dict[str, Any]] = None,
|
||||
) -> Tuple[str, Dict]:
|
||||
url = f"{api_base}/{vector_store_id}/search"
|
||||
encoded_vector_store_id = encode_url_path_segment(
|
||||
vector_store_id, field_name="vector_store_id"
|
||||
)
|
||||
url = f"{api_base}/{encoded_vector_store_id}/search"
|
||||
_, request_body = super().transform_search_vector_store_request(
|
||||
vector_store_id=vector_store_id,
|
||||
query=query,
|
||||
|
||||
@ -13,6 +13,7 @@ Model name format:
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
import litellm
|
||||
from litellm.litellm_core_utils.url_utils import encode_url_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,10 +127,11 @@ class RAGFlowConfig(OpenAIConfig):
|
||||
api_base = api_base[:-3] # Remove /v1
|
||||
|
||||
# Construct the RAGFlow-specific path
|
||||
encoded_entity_id = encode_url_path_segment(entity_id, field_name="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"
|
||||
path = f"/api/v1/agents_openai/{encoded_entity_id}/chat/completions"
|
||||
|
||||
# Ensure path starts with /
|
||||
if not path.startswith("/"):
|
||||
|
||||
@ -6,6 +6,7 @@ from httpx._types import RequestFiles
|
||||
|
||||
import litellm
|
||||
from litellm.constants import RUNWAYML_DEFAULT_API_VERSION
|
||||
from litellm.litellm_core_utils.url_utils import encode_url_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 (
|
||||
@ -334,9 +335,12 @@ class RunwayMLVideoConfig(BaseVideoConfig):
|
||||
We'll retrieve the task and extract the video URL.
|
||||
"""
|
||||
original_video_id = extract_original_video_id(video_id)
|
||||
encoded_video_id = encode_url_path_segment(
|
||||
original_video_id, field_name="video_id"
|
||||
)
|
||||
|
||||
# Get task status to retrieve video URL
|
||||
url = f"{api_base}/tasks/{original_video_id}"
|
||||
url = f"{api_base}/tasks/{encoded_video_id}"
|
||||
|
||||
params: Dict[str, Any] = {}
|
||||
|
||||
@ -495,9 +499,12 @@ class RunwayMLVideoConfig(BaseVideoConfig):
|
||||
RunwayML uses task cancellation.
|
||||
"""
|
||||
original_video_id = extract_original_video_id(video_id)
|
||||
encoded_video_id = encode_url_path_segment(
|
||||
original_video_id, field_name="video_id"
|
||||
)
|
||||
|
||||
# Construct the URL for task cancellation
|
||||
url = f"{api_base}/tasks/{original_video_id}/cancel"
|
||||
url = f"{api_base}/tasks/{encoded_video_id}/cancel"
|
||||
|
||||
data: Dict[str, Any] = {}
|
||||
|
||||
@ -533,9 +540,12 @@ class RunwayMLVideoConfig(BaseVideoConfig):
|
||||
RunwayML uses GET /v1/tasks/{task_id} to retrieve task status.
|
||||
"""
|
||||
original_video_id = extract_original_video_id(video_id)
|
||||
encoded_video_id = encode_url_path_segment(
|
||||
original_video_id, field_name="video_id"
|
||||
)
|
||||
|
||||
# Construct the full URL for task status retrieval
|
||||
url = f"{api_base}/tasks/{original_video_id}"
|
||||
url = f"{api_base}/tasks/{encoded_video_id}"
|
||||
|
||||
# Empty dict for GET request (no body)
|
||||
data: Dict[str, Any] = {}
|
||||
|
||||
@ -17,6 +17,7 @@ from pydantic import fields as pyd_fields
|
||||
import litellm
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.litellm_core_utils.core_helpers import process_response_headers
|
||||
from litellm.litellm_core_utils.url_utils import encode_url_path_segment
|
||||
from litellm.litellm_core_utils.llm_response_utils.convert_dict_to_response import (
|
||||
_safe_convert_created_field,
|
||||
)
|
||||
@ -300,7 +301,10 @@ class VolcEngineResponsesAPIConfig(OpenAIResponsesAPIConfig):
|
||||
litellm_params: GenericLiteLLMParams,
|
||||
headers: dict,
|
||||
) -> Tuple[str, Dict]:
|
||||
url = f"{api_base}/{response_id}"
|
||||
encoded_response_id = encode_url_path_segment(
|
||||
response_id, field_name="response_id"
|
||||
)
|
||||
url = f"{api_base}/{encoded_response_id}"
|
||||
data: Dict = {}
|
||||
return url, data
|
||||
|
||||
@ -333,7 +337,10 @@ class VolcEngineResponsesAPIConfig(OpenAIResponsesAPIConfig):
|
||||
litellm_params: GenericLiteLLMParams,
|
||||
headers: dict,
|
||||
) -> Tuple[str, Dict]:
|
||||
url = f"{api_base}/{response_id}"
|
||||
encoded_response_id = encode_url_path_segment(
|
||||
response_id, field_name="response_id"
|
||||
)
|
||||
url = f"{api_base}/{encoded_response_id}"
|
||||
data: Dict = {}
|
||||
return url, data
|
||||
|
||||
@ -372,7 +379,10 @@ class VolcEngineResponsesAPIConfig(OpenAIResponsesAPIConfig):
|
||||
limit: int = 20,
|
||||
order: Literal["asc", "desc"] = "desc",
|
||||
) -> Tuple[str, Dict]:
|
||||
url = f"{api_base}/{response_id}/input_items"
|
||||
encoded_response_id = encode_url_path_segment(
|
||||
response_id, field_name="response_id"
|
||||
)
|
||||
url = f"{api_base}/{encoded_response_id}/input_items"
|
||||
params: Dict[str, Any] = {}
|
||||
if after is not None:
|
||||
params["after"] = after
|
||||
@ -408,7 +418,10 @@ class VolcEngineResponsesAPIConfig(OpenAIResponsesAPIConfig):
|
||||
litellm_params: GenericLiteLLMParams,
|
||||
headers: dict,
|
||||
) -> Tuple[str, Dict]:
|
||||
url = f"{api_base}/{response_id}/cancel"
|
||||
encoded_response_id = encode_url_path_segment(
|
||||
response_id, field_name="response_id"
|
||||
)
|
||||
url = f"{api_base}/{encoded_response_id}/cancel"
|
||||
data: Dict = {}
|
||||
return url, data
|
||||
|
||||
|
||||
@ -323,6 +323,29 @@ class TestAzureContainerConfig:
|
||||
assert url_fc == expected_fc
|
||||
assert url_fc.index("/content") < url_fc.index("?")
|
||||
|
||||
def test_transform_requests_encode_path_ids_before_query_string(self):
|
||||
from litellm.types.router import GenericLiteLLMParams
|
||||
|
||||
api_base = (
|
||||
"https://my-resource.openai.azure.com/openai/v1/containers"
|
||||
"?api-version=v1"
|
||||
)
|
||||
|
||||
url, _ = self.config.transform_container_file_content_request(
|
||||
container_id="../../other",
|
||||
file_id="file?download=1#frag",
|
||||
api_base=api_base,
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={},
|
||||
)
|
||||
|
||||
expected_url = (
|
||||
"https://my-resource.openai.azure.com/openai/v1/containers/"
|
||||
"..%2F..%2Fother/files/file%3Fdownload%3D1%23frag/content"
|
||||
"?api-version=v1"
|
||||
)
|
||||
assert url == expected_url
|
||||
|
||||
def test_provider_config_manager_returns_azure_config(self):
|
||||
from litellm.types.utils import LlmProviders
|
||||
from litellm.utils import ProviderConfigManager
|
||||
|
||||
28
tests/test_litellm/containers/test_container_handler_url.py
Normal file
28
tests/test_litellm/containers/test_container_handler_url.py
Normal file
@ -0,0 +1,28 @@
|
||||
import pytest
|
||||
|
||||
from litellm.llms.custom_httpx.container_handler import _build_url
|
||||
|
||||
|
||||
def test_build_url_encodes_path_params_and_preserves_query():
|
||||
url = _build_url(
|
||||
api_base="https://example.com/v1/containers?api-version=v1",
|
||||
path_template="/containers/{container_id}/files/{file_id}/content",
|
||||
path_params={
|
||||
"container_id": "../../containers/other",
|
||||
"file_id": "file?download=1#frag",
|
||||
},
|
||||
)
|
||||
|
||||
assert (
|
||||
url
|
||||
== "https://example.com/v1/containers/..%2F..%2Fcontainers%2Fother/files/file%3Fdownload%3D1%23frag/content?api-version=v1"
|
||||
)
|
||||
|
||||
|
||||
def test_build_url_rejects_dot_segment_path_param():
|
||||
with pytest.raises(ValueError, match="container_id cannot be a dot path segment"):
|
||||
_build_url(
|
||||
api_base="https://example.com/v1/containers",
|
||||
path_template="/containers/{container_id}",
|
||||
path_params={"container_id": ".."},
|
||||
)
|
||||
@ -230,6 +230,23 @@ class TestOpenAIContainerTransformation:
|
||||
assert url == f"{api_base}/{container_id}"
|
||||
assert params == {} # No query params for retrieve
|
||||
|
||||
def test_transform_container_retrieve_request_encodes_path_traversal(self):
|
||||
"""Test container IDs are treated as a single upstream path segment."""
|
||||
api_base = "https://api.openai.com/v1/containers"
|
||||
|
||||
url, params = self.config.transform_container_retrieve_request(
|
||||
container_id="../../vector_stores?x=1#frag",
|
||||
api_base=api_base,
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert (
|
||||
url
|
||||
== "https://api.openai.com/v1/containers/..%2F..%2Fvector_stores%3Fx%3D1%23frag"
|
||||
)
|
||||
assert params == {}
|
||||
|
||||
def test_transform_container_retrieve_response(self):
|
||||
"""Test container retrieve response transformation."""
|
||||
# Mock HTTP response
|
||||
|
||||
@ -147,6 +147,21 @@ class TestInteractionOperationUrls:
|
||||
assert "secret-key" not in url
|
||||
assert expected_suffix in url
|
||||
|
||||
def test_interaction_id_is_encoded_as_one_path_segment(self, config):
|
||||
with patch(_PATCH_GET_API_KEY, return_value="secret-key"):
|
||||
url, params = config.transform_cancel_interaction_request(
|
||||
interaction_id="../../interactions/other?x=1#frag",
|
||||
api_base="https://generativelanguage.googleapis.com",
|
||||
litellm_params=GenericLiteLLMParams(api_key="secret-key"),
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert (
|
||||
url
|
||||
== "https://generativelanguage.googleapis.com/v1beta/interactions/..%2F..%2Finteractions%2Fother%3Fx%3D1%23frag:cancel"
|
||||
)
|
||||
assert params == {}
|
||||
|
||||
def test_get_interaction_raises_without_key(self, config):
|
||||
with patch(_PATCH_GET_API_KEY, return_value=None):
|
||||
with pytest.raises(ValueError, match="Google API key is required"):
|
||||
|
||||
@ -4,7 +4,12 @@ import pytest
|
||||
|
||||
import litellm
|
||||
from litellm.litellm_core_utils import url_utils
|
||||
from litellm.litellm_core_utils.url_utils import SSRFError, _is_blocked_ip, validate_url
|
||||
from litellm.litellm_core_utils.url_utils import (
|
||||
SSRFError,
|
||||
_is_blocked_ip,
|
||||
encode_url_path_segment,
|
||||
validate_url,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -80,6 +85,18 @@ class TestIsBlockedIp:
|
||||
assert _is_blocked_ip("::ffff:168.63.129.16") is True
|
||||
|
||||
|
||||
class TestEncodeUrlPathSegment:
|
||||
def test_encodes_path_delimiters_and_query_markers(self):
|
||||
encoded = encode_url_path_segment("../../v1/files?limit=1#frag")
|
||||
|
||||
assert encoded == "..%2F..%2Fv1%2Ffiles%3Flimit%3D1%23frag"
|
||||
|
||||
@pytest.mark.parametrize("value", ["", ".", "..", None])
|
||||
def test_rejects_empty_and_dot_segments(self, value):
|
||||
with pytest.raises(ValueError):
|
||||
encode_url_path_segment(value, field_name="resource_id")
|
||||
|
||||
|
||||
class TestValidateUrl:
|
||||
def test_blocks_loopback(self):
|
||||
with pytest.raises(SSRFError):
|
||||
|
||||
@ -180,6 +180,19 @@ class TestAnthropicFilesConfig:
|
||||
assert url == "https://custom.api.com/v1/files/file-abc123"
|
||||
assert params == {}
|
||||
|
||||
def test_transform_retrieve_file_request_encodes_path_traversal(self):
|
||||
url, params = self.config.transform_retrieve_file_request(
|
||||
file_id="../../v1/messages/batches?limit=1#frag",
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
)
|
||||
|
||||
assert (
|
||||
url
|
||||
== f"{ANTHROPIC_FILES_API_BASE}/v1/files/..%2F..%2Fv1%2Fmessages%2Fbatches%3Flimit%3D1%23frag"
|
||||
)
|
||||
assert params == {}
|
||||
|
||||
def test_transform_retrieve_file_response(self):
|
||||
mock_response = Mock(spec=httpx.Response)
|
||||
mock_response.json.return_value = {
|
||||
@ -296,6 +309,14 @@ class TestAnthropicFilesConfig:
|
||||
assert url == f"{ANTHROPIC_FILES_API_BASE}/v1/files/file-abc123/content"
|
||||
assert params == {}
|
||||
|
||||
def test_transform_file_content_request_rejects_dot_segment(self):
|
||||
with pytest.raises(ValueError, match="file_id cannot be a dot path segment"):
|
||||
self.config.transform_file_content_request(
|
||||
file_content_request={"file_id": ".."},
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
)
|
||||
|
||||
def test_transform_file_content_response(self):
|
||||
mock_response = Mock(spec=httpx.Response)
|
||||
result = self.config.transform_file_content_response(
|
||||
|
||||
@ -96,6 +96,28 @@ def test_get_complete_url():
|
||||
assert result == expected
|
||||
|
||||
|
||||
@pytest.mark.serial
|
||||
def test_response_id_path_requests_encode_response_id():
|
||||
config = AzureOpenAIResponsesAPIConfig()
|
||||
api_base = (
|
||||
"https://litellm8397336933.openai.azure.com/openai/responses"
|
||||
"?api-version=2024-05-01-preview"
|
||||
)
|
||||
|
||||
url, params = config.transform_cancel_response_api_request(
|
||||
response_id="../../responses/other?x=1#frag",
|
||||
api_base=api_base,
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert (
|
||||
url
|
||||
== "https://litellm8397336933.openai.azure.com/openai/responses/..%2F..%2Fresponses%2Fother%3Fx%3D1%23frag/cancel?api-version=2024-05-01-preview"
|
||||
)
|
||||
assert params == {}
|
||||
|
||||
|
||||
@pytest.mark.serial
|
||||
def test_azure_o_series_responses_api_supported_params():
|
||||
"""Test that Azure OpenAI O-series responses API excludes temperature from supported parameters."""
|
||||
|
||||
@ -167,3 +167,24 @@ def test_tool_name_sanitization():
|
||||
]
|
||||
# Should be sanitized: only [a-zA-Z0-9_]
|
||||
assert tool_name == "my_tool_"
|
||||
|
||||
|
||||
def test_count_tokens_endpoint_encodes_model_id(monkeypatch):
|
||||
"""Test model IDs are treated as a single Bedrock path segment."""
|
||||
config = BedrockCountTokensConfig()
|
||||
|
||||
monkeypatch.setattr(
|
||||
config,
|
||||
"get_runtime_endpoint",
|
||||
lambda **kwargs: ("https://bedrock-runtime.us-east-1.amazonaws.com", None),
|
||||
)
|
||||
|
||||
endpoint = config.get_bedrock_count_tokens_endpoint(
|
||||
model="bedrock/../../model/other?x=1#frag",
|
||||
aws_region_name="us-east-1",
|
||||
)
|
||||
|
||||
assert (
|
||||
endpoint
|
||||
== "https://bedrock-runtime.us-east-1.amazonaws.com/model/..%2F..%2Fmodel%2Fother%3Fx%3D1%23frag/count-tokens"
|
||||
)
|
||||
|
||||
@ -28,6 +28,28 @@ def test_transform_search_request():
|
||||
assert body["retrievalQuery"].get("text") == "hello"
|
||||
|
||||
|
||||
def test_transform_search_request_encodes_vector_store_id():
|
||||
config = BedrockVectorStoreConfig()
|
||||
mock_log = MagicMock()
|
||||
mock_log.model_call_details = {}
|
||||
|
||||
url, body = config.transform_search_vector_store_request(
|
||||
vector_store_id="../../knowledgebases/other?x=1#frag",
|
||||
query="hello",
|
||||
vector_store_search_optional_params={},
|
||||
api_base="https://bedrock-agent-runtime.us-west-2.amazonaws.com/knowledgebases",
|
||||
litellm_logging_obj=mock_log,
|
||||
litellm_params={},
|
||||
extra_body=None,
|
||||
)
|
||||
|
||||
assert (
|
||||
url
|
||||
== "https://bedrock-agent-runtime.us-west-2.amazonaws.com/knowledgebases/..%2F..%2Fknowledgebases%2Fother%3Fx%3D1%23frag/retrieve"
|
||||
)
|
||||
assert body["retrievalQuery"].get("text") == "hello"
|
||||
|
||||
|
||||
def test_transform_search_request_uses_only_retrieval_config_from_extra_body():
|
||||
config = BedrockVectorStoreConfig()
|
||||
mock_log = MagicMock()
|
||||
|
||||
@ -2,6 +2,7 @@ import os
|
||||
import sys
|
||||
import pytest
|
||||
import json
|
||||
from urllib.parse import quote
|
||||
|
||||
# Adds the parent directory to the system path
|
||||
sys.path.insert(0, os.path.abspath("../../../../.."))
|
||||
@ -69,7 +70,7 @@ class TestBytezChatConfig:
|
||||
}
|
||||
|
||||
# Mock the HTTP request
|
||||
respx_mock.post(f"{API_BASE}/{TEST_MODEL_NAME}").respond(
|
||||
respx_mock.post(f"{API_BASE}/{quote(TEST_MODEL_NAME, safe='')}").respond(
|
||||
json={
|
||||
"error": None,
|
||||
"output": output,
|
||||
@ -87,6 +88,19 @@ class TestBytezChatConfig:
|
||||
|
||||
assert response.choices[0].message.content == output_content # type: ignore
|
||||
|
||||
def test_get_complete_url_encodes_model_path_segment(self):
|
||||
config = BytezChatConfig()
|
||||
|
||||
url = config.get_complete_url(
|
||||
api_base=API_BASE,
|
||||
api_key=TEST_API_KEY,
|
||||
model="../../models/other?x=1#frag",
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
)
|
||||
|
||||
assert url == f"{API_BASE}/..%2F..%2Fmodels%2Fother%3Fx%3D1%23frag"
|
||||
|
||||
def test_bytez_messages_adaptation(self):
|
||||
cases = [
|
||||
dict(
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
from litellm.llms.cloudflare.chat.transformation import CloudflareChatConfig
|
||||
|
||||
|
||||
def test_get_complete_url_encodes_model_path_segment():
|
||||
config = CloudflareChatConfig()
|
||||
|
||||
url = config.get_complete_url(
|
||||
api_base="https://api.cloudflare.com/client/v4/accounts/acct/ai/run/",
|
||||
api_key="cf-key",
|
||||
model="../../accounts/other?x=1#frag",
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
)
|
||||
|
||||
assert (
|
||||
url
|
||||
== "https://api.cloudflare.com/client/v4/accounts/acct/ai/run/..%2F..%2Faccounts%2Fother%3Fx%3D1%23frag"
|
||||
)
|
||||
@ -58,3 +58,18 @@ def test_transform_responses_api_request_adds_manus_params():
|
||||
assert result["agent_profile"] == "manus-1.6"
|
||||
assert "input" in result
|
||||
assert "model" in result
|
||||
|
||||
|
||||
def test_get_response_request_encodes_response_id():
|
||||
"""Test response IDs are encoded before being appended to Manus URLs."""
|
||||
config = ManusResponsesAPIConfig()
|
||||
|
||||
url, params = config.transform_get_response_api_request(
|
||||
response_id="../../files?x=1#frag",
|
||||
api_base="https://api.manus.im/v1/responses",
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert url == "https://api.manus.im/v1/responses/..%2F..%2Ffiles%3Fx%3D1%23frag"
|
||||
assert params == {}
|
||||
|
||||
@ -49,6 +49,17 @@ def test_get_complete_url_with_eval_id(config: OpenAIEvalsConfig):
|
||||
assert url == "https://api.openai.com/v1/evals/eval_123"
|
||||
|
||||
|
||||
def test_get_complete_url_encodes_eval_id(config: OpenAIEvalsConfig):
|
||||
"""Test eval_id is treated as a single path segment."""
|
||||
url = config.get_complete_url(
|
||||
api_base="https://api.openai.com",
|
||||
endpoint="evals",
|
||||
eval_id="../../files?x=1#frag",
|
||||
)
|
||||
|
||||
assert url == "https://api.openai.com/v1/evals/..%2F..%2Ffiles%3Fx%3D1%23frag"
|
||||
|
||||
|
||||
def test_get_complete_url_without_eval_id(config: OpenAIEvalsConfig):
|
||||
"""Test URL construction without eval_id"""
|
||||
url = config.get_complete_url(
|
||||
@ -253,3 +264,20 @@ def test_transform_cancel_eval_response(config: OpenAIEvalsConfig):
|
||||
|
||||
assert result.id == "eval_123"
|
||||
assert result.object == "eval"
|
||||
|
||||
|
||||
def test_transform_run_requests_encode_eval_and_run_ids(config: OpenAIEvalsConfig):
|
||||
"""Test run path IDs are treated as single path segments."""
|
||||
url, _, request_body = config.transform_cancel_run_request(
|
||||
eval_id="../../evals?x=1#frag",
|
||||
run_id="../runs#other",
|
||||
api_base="https://api.openai.com",
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert (
|
||||
url
|
||||
== "https://api.openai.com/v1/evals/..%2F..%2Fevals%3Fx%3D1%23frag/runs/..%2Fruns%23other/cancel"
|
||||
)
|
||||
assert request_body == {}
|
||||
|
||||
@ -265,6 +265,24 @@ class TestOpenAIResponsesAPIConfig:
|
||||
|
||||
assert result == "https://custom-openai.example.com/v1/responses"
|
||||
|
||||
def test_response_id_path_requests_encode_response_id(self):
|
||||
"""Test response_id is treated as one upstream URL path segment."""
|
||||
api_base = "https://custom-openai.example.com/v1/responses"
|
||||
response_id = "../../files?x=1#frag"
|
||||
|
||||
url, data = self.config.transform_list_input_items_request(
|
||||
response_id=response_id,
|
||||
api_base=api_base,
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert (
|
||||
url
|
||||
== "https://custom-openai.example.com/v1/responses/..%2F..%2Ffiles%3Fx%3D1%23frag/input_items"
|
||||
)
|
||||
assert data["limit"] == 20
|
||||
|
||||
def test_get_event_model_class_generic_event(self):
|
||||
"""Test that get_event_model_class returns the correct event model class"""
|
||||
from litellm.types.llms.openai import GenericEvent
|
||||
@ -547,7 +565,12 @@ class TestOpenAIResponsesAPIConfig:
|
||||
"""Base helper strips ``namespace`` from custom_tool_call for every provider path."""
|
||||
inp = [
|
||||
{"type": "function_call", "call_id": "a", "name": "f", "namespace": "keep"},
|
||||
{"type": "custom_tool_call", "call_id": "b", "name": "c", "namespace": "drop"},
|
||||
{
|
||||
"type": "custom_tool_call",
|
||||
"call_id": "b",
|
||||
"name": "c",
|
||||
"namespace": "drop",
|
||||
},
|
||||
]
|
||||
out = BaseResponsesAPIConfig.strip_custom_tool_call_namespace_from_responses_input(
|
||||
inp
|
||||
|
||||
@ -32,6 +32,21 @@ def test_get_complete_url(config: OpenAIVectorStoreFilesConfig):
|
||||
assert url == "https://api.example.com/v1/vector_stores/vs_123/files"
|
||||
|
||||
|
||||
def test_get_complete_url_encodes_vector_store_id(
|
||||
config: OpenAIVectorStoreFilesConfig,
|
||||
):
|
||||
url = config.get_complete_url(
|
||||
api_base="https://api.example.com/v1",
|
||||
vector_store_id="../vs_123?x=1#frag",
|
||||
litellm_params={},
|
||||
)
|
||||
|
||||
assert (
|
||||
url
|
||||
== "https://api.example.com/v1/vector_stores/..%2Fvs_123%3Fx%3D1%23frag/files"
|
||||
)
|
||||
|
||||
|
||||
def test_transform_create_request(config: OpenAIVectorStoreFilesConfig):
|
||||
api_base = "https://api.example.com/v1/vector_stores/vs_123/files"
|
||||
url, payload = config.transform_create_vector_store_file_request(
|
||||
@ -60,6 +75,22 @@ def test_transform_list_request(config: OpenAIVectorStoreFilesConfig):
|
||||
assert params == {"limit": 2, "order": "asc"}
|
||||
|
||||
|
||||
def test_transform_file_request_encodes_file_id(config: OpenAIVectorStoreFilesConfig):
|
||||
api_base = "https://api.example.com/v1/vector_stores/vs_123/files"
|
||||
|
||||
url, params = config.transform_retrieve_vector_store_file_content_request(
|
||||
vector_store_id="vs_123",
|
||||
file_id="../../files?x=1#frag",
|
||||
api_base=api_base,
|
||||
)
|
||||
|
||||
assert (
|
||||
url
|
||||
== "https://api.example.com/v1/vector_stores/vs_123/files/..%2F..%2Ffiles%3Fx%3D1%23frag/content"
|
||||
)
|
||||
assert params == {}
|
||||
|
||||
|
||||
def test_transform_create_response(config: OpenAIVectorStoreFilesConfig):
|
||||
response = httpx.Response(
|
||||
status_code=200,
|
||||
|
||||
@ -64,3 +64,21 @@ class TestOpenAIVectorStoreAPIConfig:
|
||||
for i in range(16):
|
||||
assert f"key_{i}" in request_body["metadata"]
|
||||
assert request_body["metadata"][f"key_{i}"] == f"value_{i}"
|
||||
|
||||
def test_transform_search_vector_store_request_encodes_vector_store_id(self):
|
||||
config = OpenAIVectorStoreConfig()
|
||||
|
||||
url, request_body = config.transform_search_vector_store_request(
|
||||
vector_store_id="../../files?x=1#frag",
|
||||
query="hello",
|
||||
vector_store_search_optional_params={},
|
||||
api_base="https://api.openai.com/v1/vector_stores",
|
||||
litellm_logging_obj=None, # type: ignore[arg-type]
|
||||
litellm_params={},
|
||||
)
|
||||
|
||||
assert (
|
||||
url
|
||||
== "https://api.openai.com/v1/vector_stores/..%2F..%2Ffiles%3Fx%3D1%23frag/search"
|
||||
)
|
||||
assert request_body["query"] == "hello"
|
||||
|
||||
@ -0,0 +1,42 @@
|
||||
from litellm.llms.openai.videos.transformation import OpenAIVideoConfig
|
||||
from litellm.types.router import GenericLiteLLMParams
|
||||
from litellm.types.videos.utils import encode_character_id_with_provider
|
||||
|
||||
|
||||
def test_video_content_request_encodes_video_id_path_segment():
|
||||
config = OpenAIVideoConfig()
|
||||
|
||||
url, params = config.transform_video_content_request(
|
||||
video_id="../../responses?x=1#frag",
|
||||
api_base="https://api.openai.com/v1/videos",
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert (
|
||||
url
|
||||
== "https://api.openai.com/v1/videos/..%2F..%2Fresponses%3Fx%3D1%23frag/content"
|
||||
)
|
||||
assert params == {}
|
||||
|
||||
|
||||
def test_wrapped_character_id_is_decoded_then_encoded_as_path_segment():
|
||||
config = OpenAIVideoConfig()
|
||||
character_id = encode_character_id_with_provider(
|
||||
"../../characters?x=1#frag",
|
||||
provider="openai",
|
||||
model_id="sora",
|
||||
)
|
||||
|
||||
url, params = config.transform_video_get_character_request(
|
||||
character_id=character_id,
|
||||
api_base="https://api.openai.com/v1/videos",
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert (
|
||||
url
|
||||
== "https://api.openai.com/v1/videos/characters/..%2F..%2Fcharacters%3Fx%3D1%23frag"
|
||||
)
|
||||
assert params == {}
|
||||
@ -141,6 +141,24 @@ class TestPGVectorStoreConfig:
|
||||
assert headers["Authorization"] == "Bearer test_key"
|
||||
assert url == "https://example.com/v1/vector_stores"
|
||||
|
||||
def test_search_request_encodes_vector_store_id(self):
|
||||
config = PGVectorStoreConfig()
|
||||
|
||||
url, request_body = config.transform_search_vector_store_request(
|
||||
vector_store_id="../../files?x=1#frag",
|
||||
query="hello",
|
||||
vector_store_search_optional_params={},
|
||||
api_base="https://example.com/v1/vector_stores",
|
||||
litellm_logging_obj=Mock(),
|
||||
litellm_params={},
|
||||
)
|
||||
|
||||
assert (
|
||||
url
|
||||
== "https://example.com/v1/vector_stores/..%2F..%2Ffiles%3Fx%3D1%23frag/search"
|
||||
)
|
||||
assert request_body["query"] == "hello"
|
||||
|
||||
def test_environment_variable_support(self):
|
||||
"""
|
||||
Test that environment variables are supported for configuration.
|
||||
|
||||
@ -117,6 +117,24 @@ class TestRAGFlowChatTransformation:
|
||||
== "http://localhost:9380/api/v1/agents_openai/my-agent-id/chat/completions"
|
||||
)
|
||||
|
||||
def test_get_complete_url_encodes_entity_id(self):
|
||||
"""Test RAGFlow chat IDs are encoded as one upstream path segment."""
|
||||
config = RAGFlowConfig()
|
||||
|
||||
url = config.get_complete_url(
|
||||
api_base="http://localhost:9380",
|
||||
api_key=None,
|
||||
model="ragflow/chat/..%2F..%2Fagents_openai%2Fother/gpt-4o-mini",
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
stream=False,
|
||||
)
|
||||
|
||||
assert (
|
||||
url
|
||||
== "http://localhost:9380/api/v1/chats_openai/..%252F..%252Fagents_openai%252Fother/chat/completions"
|
||||
)
|
||||
|
||||
def test_get_complete_url_strips_v1(self):
|
||||
"""Test URL construction when api_base ends with /v1."""
|
||||
config = RAGFlowConfig()
|
||||
|
||||
@ -134,6 +134,21 @@ class TestRunwayMLVideoTransformation:
|
||||
with pytest.raises(ValueError, match="still processing"):
|
||||
self.config._extract_video_url_from_response(processing_response)
|
||||
|
||||
def test_transform_video_status_encodes_video_id_path_segment(self):
|
||||
"""Test task IDs are encoded before being appended to Runway URLs."""
|
||||
url, params = self.config.transform_video_status_retrieve_request(
|
||||
video_id="../../tasks/other?x=1#frag",
|
||||
api_base="https://api.dev.runwayml.com/v1",
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert (
|
||||
url
|
||||
== "https://api.dev.runwayml.com/v1/tasks/..%2F..%2Ftasks%2Fother%3Fx%3D1%23frag"
|
||||
)
|
||||
assert params == {}
|
||||
|
||||
def test_full_video_workflow(self):
|
||||
"""Test complete video generation workflow from creation to status check."""
|
||||
config = RunwayMLVideoConfig()
|
||||
|
||||
@ -101,6 +101,23 @@ class TestVolcengineResponsesAPITransformation:
|
||||
)
|
||||
assert api_base_full == "https://custom.volc.com/api/v3/responses"
|
||||
|
||||
def test_response_id_path_requests_encode_response_id(self):
|
||||
"""response_id should be encoded before building Volcengine URLs."""
|
||||
config = VolcEngineResponsesAPIConfig()
|
||||
|
||||
url, params = config.transform_cancel_response_api_request(
|
||||
response_id="../../responses/other?x=1#frag",
|
||||
api_base="https://custom.volc.com/api/v3/responses",
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert (
|
||||
url
|
||||
== "https://custom.volc.com/api/v3/responses/..%2F..%2Fresponses%2Fother%3Fx%3D1%23frag/cancel"
|
||||
)
|
||||
assert params == {}
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"litellm_params, expected_key",
|
||||
[
|
||||
|
||||
@ -70,6 +70,15 @@ class TestAnthropicSkillsConfigURLConstruction:
|
||||
)
|
||||
assert url == f"{FAKE_API_BASE}/v1/skills/skill_abc123"
|
||||
|
||||
def test_url_with_skill_id_encodes_path_segment(self):
|
||||
url = self.config.get_complete_url(
|
||||
api_base=FAKE_API_BASE,
|
||||
endpoint="skills",
|
||||
skill_id="../../files?x=1#frag",
|
||||
)
|
||||
|
||||
assert url == f"{FAKE_API_BASE}/v1/skills/..%2F..%2Ffiles%3Fx%3D1%23frag"
|
||||
|
||||
def test_url_falls_back_to_anthropic_default(self):
|
||||
with patch(
|
||||
"litellm.llms.anthropic.common_utils.AnthropicModelInfo.get_api_base",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user