fix(llms): sanitize trailing slashes in provider get_complete_url

This commit is contained in:
Raushankumar0720 2026-09-24 11:12:28 +05:30
parent b21b20ed13
commit 883db28407
15 changed files with 151 additions and 2 deletions

View file

@ -366,6 +366,8 @@ class DeepSeekChatConfig(OpenAIGPTConfig):
if not api_base:
api_base = "https://api.deepseek.com/beta"
api_base = api_base.rstrip("/")
if not api_base.endswith("/chat/completions"):
api_base = f"{api_base}/chat/completions"

View file

@ -71,6 +71,8 @@ class HerokuChatConfig(OpenAIGPTConfig):
"No api base was set. Please provide an api_base, or set the HEROKU_API_BASE environment variable."
)
api_base = api_base.rstrip("/")
if not api_base.endswith("/v1/chat/completions"):
api_base = f"{api_base}/v1/chat/completions"

View file

@ -95,7 +95,12 @@ class JinaAIEmbeddingConfig(BaseEmbeddingConfig):
litellm_params: dict,
stream: bool | None = None,
) -> str:
return f"{api_base}/embeddings" if api_base else "https://api.jina.ai/v1/embeddings"
if api_base:
api_base = api_base.rstrip("/")
if not api_base.endswith("/embeddings"):
return f"{api_base}/embeddings"
return api_base
return "https://api.jina.ai/v1/embeddings"
def transform_embedding_request(
self,

View file

@ -82,6 +82,8 @@ class ModelScopeChatConfig(OpenAIGPTConfig):
if not api_base:
api_base = self.DEFAULT_BASE_URL
api_base = api_base.rstrip("/")
if not api_base.endswith("/chat/completions"):
api_base = f"{api_base}/chat/completions"

View file

@ -89,6 +89,8 @@ class MoonshotChatConfig(OpenAIGPTConfig):
if not api_base:
api_base = "https://api.moonshot.ai/v1"
api_base = api_base.rstrip("/")
if not api_base.endswith("/chat/completions"):
api_base = f"{api_base}/chat/completions"

View file

@ -220,6 +220,7 @@ class OllamaChatConfig(BaseConfig):
"""
if api_base is None:
api_base = "http://localhost:11434"
api_base = api_base.rstrip("/")
if api_base.endswith("/api/chat"):
url = api_base
else:

View file

@ -470,6 +470,7 @@ class OllamaConfig(BaseConfig):
"""
if api_base is None:
api_base = "http://localhost:11434"
api_base = api_base.rstrip("/")
if api_base.endswith("/api/generate"):
url = api_base
else:

View file

@ -61,6 +61,7 @@ class PerplexityEmbeddingConfig(BaseEmbeddingConfig):
stream: bool | None = None,
) -> str:
if api_base:
api_base = api_base.rstrip("/")
if not api_base.endswith("/embeddings"):
api_base = f"{api_base}/v1/embeddings"
return api_base

View file

@ -236,6 +236,8 @@ class SnowflakeConfig(SnowflakeBaseConfig, OpenAIGPTConfig):
stream: bool | None = None,
) -> str:
api_base = self._get_api_base(api_base, optional_params)
if api_base:
api_base = api_base.rstrip("/")
if _is_claude_model(model):
return f"{api_base}/cortex/v1/messages"
return f"{api_base}/cortex/v1/chat/completions"

View file

@ -26,6 +26,8 @@ class SnowflakeEmbeddingConfig(SnowflakeBaseConfig, BaseEmbeddingConfig):
stream: bool | None = None,
) -> str:
api_base = self._get_api_base(api_base, optional_params)
if api_base:
api_base = api_base.rstrip("/")
return f"{api_base}/cortex/inference:embed"

View file

@ -39,7 +39,7 @@ class TopazImageVariationConfig(TopazModelInfo, BaseImageVariationConfig):
litellm_params: dict,
stream: bool | None = None,
) -> str:
api_base = api_base or "https://api.topazlabs.com"
api_base = (api_base or "https://api.topazlabs.com").rstrip("/")
return f"{api_base}/image/v1/enhance"
def map_openai_params(

View file

@ -46,6 +46,7 @@ class VoyageEmbeddingConfig(BaseEmbeddingConfig):
stream: bool | None = None,
) -> str:
if api_base:
api_base = api_base.rstrip("/")
if not api_base.endswith("/embeddings"):
api_base = f"{api_base}/embeddings"
return api_base

View file

@ -55,6 +55,7 @@ class VoyageContextualEmbeddingConfig(BaseEmbeddingConfig):
stream: bool | None = None,
) -> str:
if api_base:
api_base = api_base.rstrip("/")
if not api_base.endswith("/contextualizedembeddings"):
api_base = f"{api_base}/contextualizedembeddings"
return api_base

View file

@ -55,6 +55,7 @@ class VoyageMultimodalEmbeddingConfig(BaseEmbeddingConfig):
stream: bool | None = None,
) -> str:
if api_base:
api_base = api_base.rstrip("/")
if not api_base.endswith("/multimodalembeddings"):
api_base = f"{api_base}/multimodalembeddings"
return api_base

View file

@ -0,0 +1,126 @@
from litellm.llms.deepseek.chat.transformation import DeepSeekChatConfig
from litellm.llms.ollama.chat.transformation import OllamaChatConfig
from litellm.llms.ollama.completion.transformation import OllamaConfig
from litellm.llms.moonshot.chat.transformation import MoonshotChatConfig
from litellm.llms.modelscope.chat.transformation import ModelScopeChatConfig
from litellm.llms.jina_ai.embedding.transformation import JinaAIEmbeddingConfig
from litellm.llms.voyage.embedding.transformation import VoyageEmbeddingConfig
from litellm.llms.voyage.embedding.transformation_contextual import VoyageContextualEmbeddingConfig
from litellm.llms.voyage.embedding.transformation_multimodal import VoyageMultimodalEmbeddingConfig
from litellm.llms.perplexity.embedding.transformation import PerplexityEmbeddingConfig
from litellm.llms.snowflake.chat.transformation import SnowflakeConfig
from litellm.llms.snowflake.embedding.transformation import SnowflakeEmbeddingConfig
from litellm.llms.heroku.chat.transformation import HerokuChatConfig
from litellm.llms.topaz.image_variations.transformation import TopazImageVariationConfig
def test_deepseek_get_complete_url_trailing_slash():
config = DeepSeekChatConfig()
url_without_slash = config.get_complete_url(api_base="https://custom.deepseek.com", api_key=None, model="deepseek-chat", optional_params={}, litellm_params={})
url_with_slash = config.get_complete_url(api_base="https://custom.deepseek.com/", api_key=None, model="deepseek-chat", optional_params={}, litellm_params={})
assert url_without_slash == "https://custom.deepseek.com/chat/completions"
assert url_with_slash == "https://custom.deepseek.com/chat/completions"
assert "//chat/completions" not in url_with_slash
def test_ollama_get_complete_url_trailing_slash():
chat_config = OllamaChatConfig()
url_without_slash = chat_config.get_complete_url(api_base="http://localhost:11434", api_key=None, model="llama3", optional_params={}, litellm_params={})
url_with_slash = chat_config.get_complete_url(api_base="http://localhost:11434/", api_key=None, model="llama3", optional_params={}, litellm_params={})
assert url_without_slash == "http://localhost:11434/api/chat"
assert url_with_slash == "http://localhost:11434/api/chat"
assert "//api/chat" not in url_with_slash
completion_config = OllamaConfig()
comp_url_without_slash = completion_config.get_complete_url(api_base="http://localhost:11434", api_key=None, model="llama3", optional_params={}, litellm_params={})
comp_url_with_slash = completion_config.get_complete_url(api_base="http://localhost:11434/", api_key=None, model="llama3", optional_params={}, litellm_params={})
assert comp_url_without_slash == "http://localhost:11434/api/generate"
assert comp_url_with_slash == "http://localhost:11434/api/generate"
assert "//api/generate" not in comp_url_with_slash
def test_moonshot_get_complete_url_trailing_slash():
config = MoonshotChatConfig()
url_without_slash = config.get_complete_url(api_base="https://api.moonshot.ai/v1", api_key=None, model="moonshot-v1-8k", optional_params={}, litellm_params={})
url_with_slash = config.get_complete_url(api_base="https://api.moonshot.ai/v1/", api_key=None, model="moonshot-v1-8k", optional_params={}, litellm_params={})
assert url_without_slash == "https://api.moonshot.ai/v1/chat/completions"
assert url_with_slash == "https://api.moonshot.ai/v1/chat/completions"
assert "//chat/completions" not in url_with_slash
def test_modelscope_get_complete_url_trailing_slash():
config = ModelScopeChatConfig()
url_without_slash = config.get_complete_url(api_base="https://api-inference.modelscope.cn/v1", api_key=None, model="qwen", optional_params={}, litellm_params={})
url_with_slash = config.get_complete_url(api_base="https://api-inference.modelscope.cn/v1/", api_key=None, model="qwen", optional_params={}, litellm_params={})
assert url_without_slash == "https://api-inference.modelscope.cn/v1/chat/completions"
assert url_with_slash == "https://api-inference.modelscope.cn/v1/chat/completions"
assert "//chat/completions" not in url_with_slash
def test_jina_ai_get_complete_url_trailing_slash():
config = JinaAIEmbeddingConfig()
url_without_slash = config.get_complete_url(api_base="https://api.jina.ai/v1", api_key=None, model="jina-embeddings-v2", optional_params={}, litellm_params={})
url_with_slash = config.get_complete_url(api_base="https://api.jina.ai/v1/", api_key=None, model="jina-embeddings-v2", optional_params={}, litellm_params={})
assert url_without_slash == "https://api.jina.ai/v1/embeddings"
assert url_with_slash == "https://api.jina.ai/v1/embeddings"
assert "//embeddings" not in url_with_slash
def test_voyage_get_complete_url_trailing_slash():
config = VoyageEmbeddingConfig()
url_without_slash = config.get_complete_url(api_base="https://api.voyageai.com/v1", api_key=None, model="voyage-large-2", optional_params={}, litellm_params={})
url_with_slash = config.get_complete_url(api_base="https://api.voyageai.com/v1/", api_key=None, model="voyage-large-2", optional_params={}, litellm_params={})
assert url_without_slash == "https://api.voyageai.com/v1/embeddings"
assert url_with_slash == "https://api.voyageai.com/v1/embeddings"
assert "//embeddings" not in url_with_slash
contextual_config = VoyageContextualEmbeddingConfig()
c_url = contextual_config.get_complete_url(api_base="https://api.voyageai.com/v1/", api_key=None, model="voyage-context-2", optional_params={}, litellm_params={})
assert c_url == "https://api.voyageai.com/v1/contextualizedembeddings"
assert "//contextualizedembeddings" not in c_url
multimodal_config = VoyageMultimodalEmbeddingConfig()
m_url = multimodal_config.get_complete_url(api_base="https://api.voyageai.com/v1/", api_key=None, model="voyage-multimodal-3", optional_params={}, litellm_params={})
assert m_url == "https://api.voyageai.com/v1/multimodalembeddings"
assert "//multimodalembeddings" not in m_url
def test_perplexity_get_complete_url_trailing_slash():
config = PerplexityEmbeddingConfig()
url_without_slash = config.get_complete_url(api_base="https://api.perplexity.ai", api_key=None, model="sonar-medium", optional_params={}, litellm_params={})
url_with_slash = config.get_complete_url(api_base="https://api.perplexity.ai/", api_key=None, model="sonar-medium", optional_params={}, litellm_params={})
assert url_without_slash == "https://api.perplexity.ai/v1/embeddings"
assert url_with_slash == "https://api.perplexity.ai/v1/embeddings"
assert "//v1/embeddings" not in url_with_slash
def test_snowflake_get_complete_url_trailing_slash():
config = SnowflakeConfig()
url_without_slash = config.get_complete_url(api_base="https://myorg-myaccount.snowflakecomputing.com", api_key=None, model="snowflake-arctic", optional_params={}, litellm_params={})
url_with_slash = config.get_complete_url(api_base="https://myorg-myaccount.snowflakecomputing.com/", api_key=None, model="snowflake-arctic", optional_params={}, litellm_params={})
assert url_without_slash == "https://myorg-myaccount.snowflakecomputing.com/api/v2/cortex/v1/chat/completions"
assert url_with_slash == "https://myorg-myaccount.snowflakecomputing.com/api/v2/cortex/v1/chat/completions"
assert "//cortex" not in url_with_slash
embed_config = SnowflakeEmbeddingConfig()
embed_url = embed_config.get_complete_url(api_base="https://myorg-myaccount.snowflakecomputing.com/", api_key=None, model="snowflake-arctic-embed", optional_params={}, litellm_params={})
assert embed_url == "https://myorg-myaccount.snowflakecomputing.com/api/v2/cortex/inference:embed"
assert "//cortex" not in embed_url
def test_heroku_get_complete_url_trailing_slash():
config = HerokuChatConfig()
url_without_slash = config.get_complete_url(api_base="https://my-app.herokuapp.com", api_key=None, model="claude-3-5-sonnet", optional_params={}, litellm_params={})
url_with_slash = config.get_complete_url(api_base="https://my-app.herokuapp.com/", api_key=None, model="claude-3-5-sonnet", optional_params={}, litellm_params={})
assert url_without_slash == "https://my-app.herokuapp.com/v1/chat/completions"
assert url_with_slash == "https://my-app.herokuapp.com/v1/chat/completions"
assert "//v1/chat/completions" not in url_with_slash
def test_topaz_get_complete_url_trailing_slash():
config = TopazImageVariationConfig()
url_without_slash = config.get_complete_url(api_base="https://api.topazlabs.com", api_key=None, model="topaz-v1", optional_params={}, litellm_params={})
url_with_slash = config.get_complete_url(api_base="https://api.topazlabs.com/", api_key=None, model="topaz-v1", optional_params={}, litellm_params={})
assert url_without_slash == "https://api.topazlabs.com/image/v1/enhance"
assert url_with_slash == "https://api.topazlabs.com/image/v1/enhance"
assert "//image/v1/enhance" not in url_with_slash