From ccb34a88d0aeb0c54de7a4faef3dcafb517a4775 Mon Sep 17 00:00:00 2001 From: Raushankumar0720 Date: Thu, 24 Sep 2026 11:53:27 +0530 Subject: [PATCH] refactor(llms): address review feedback on parameter rebinding and test placement --- litellm/llms/deepseek/chat/transformation.py | 11 +- litellm/llms/heroku/chat/transformation.py | 12 +- .../llms/jina_ai/embedding/transformation.py | 8 +- .../llms/modelscope/chat/transformation.py | 11 +- litellm/llms/moonshot/chat/transformation.py | 11 +- litellm/llms/ollama/chat/transformation.py | 13 +- .../llms/ollama/completion/transformation.py | 13 +- .../perplexity/embedding/transformation.py | 8 +- .../topaz/image_variations/transformation.py | 4 +- .../llms/voyage/embedding/transformation.py | 8 +- .../embedding/transformation_contextual.py | 8 +- .../embedding/transformation_multimodal.py | 8 +- .../test_provider_url_trailing_slash.py | 126 --------- .../llms/test_provider_url_trailing_slash.py | 242 ++++++++++++++++++ 14 files changed, 290 insertions(+), 193 deletions(-) delete mode 100644 tests/llm_translation/test_provider_url_trailing_slash.py create mode 100644 tests/test_litellm/llms/test_provider_url_trailing_slash.py diff --git a/litellm/llms/deepseek/chat/transformation.py b/litellm/llms/deepseek/chat/transformation.py index 1e10ef491f1..eb3a4df51a9 100644 --- a/litellm/llms/deepseek/chat/transformation.py +++ b/litellm/llms/deepseek/chat/transformation.py @@ -363,12 +363,9 @@ class DeepSeekChatConfig(OpenAIGPTConfig): """ If api_base is not provided, use the default DeepSeek /chat/completions endpoint. """ - if not api_base: - api_base = "https://api.deepseek.com/beta" + clean_api_base: Final = (api_base or "https://api.deepseek.com/beta").rstrip("/") - api_base = api_base.rstrip("/") + if not clean_api_base.endswith("/chat/completions"): + return f"{clean_api_base}/chat/completions" - if not api_base.endswith("/chat/completions"): - api_base = f"{api_base}/chat/completions" - - return api_base + return clean_api_base diff --git a/litellm/llms/heroku/chat/transformation.py b/litellm/llms/heroku/chat/transformation.py index 1aae85908f2..3abb46e0c5c 100644 --- a/litellm/llms/heroku/chat/transformation.py +++ b/litellm/llms/heroku/chat/transformation.py @@ -64,16 +64,16 @@ class HerokuChatConfig(OpenAIGPTConfig): litellm_params: dict, stream: bool | None = None, ) -> str: - api_base, _ = self._get_openai_compatible_provider_info(api_base, api_key) + resolved_base, _ = self._get_openai_compatible_provider_info(api_base, api_key) - if not api_base: + if not resolved_base: raise HerokuError( "No api base was set. Please provide an api_base, or set the HEROKU_API_BASE environment variable." ) - api_base = api_base.rstrip("/") + clean_api_base: Final = resolved_base.rstrip("/") - if not api_base.endswith("/v1/chat/completions"): - api_base = f"{api_base}/v1/chat/completions" + if not clean_api_base.endswith("/v1/chat/completions"): + return f"{clean_api_base}/v1/chat/completions" - return api_base + return clean_api_base diff --git a/litellm/llms/jina_ai/embedding/transformation.py b/litellm/llms/jina_ai/embedding/transformation.py index a746d41562e..37d791701fc 100644 --- a/litellm/llms/jina_ai/embedding/transformation.py +++ b/litellm/llms/jina_ai/embedding/transformation.py @@ -96,10 +96,10 @@ class JinaAIEmbeddingConfig(BaseEmbeddingConfig): stream: bool | None = None, ) -> str: if api_base: - api_base = api_base.rstrip("/") - if not api_base.endswith("/embeddings"): - return f"{api_base}/embeddings" - return api_base + clean_api_base: Final = api_base.rstrip("/") + if not clean_api_base.endswith("/embeddings"): + return f"{clean_api_base}/embeddings" + return clean_api_base return "https://api.jina.ai/v1/embeddings" def transform_embedding_request( diff --git a/litellm/llms/modelscope/chat/transformation.py b/litellm/llms/modelscope/chat/transformation.py index 56f1fe7acef..2f4855f6513 100644 --- a/litellm/llms/modelscope/chat/transformation.py +++ b/litellm/llms/modelscope/chat/transformation.py @@ -79,12 +79,9 @@ class ModelScopeChatConfig(OpenAIGPTConfig): """ If api_base is not provided, use the default ModelScope /chat/completions endpoint. """ - if not api_base: - api_base = self.DEFAULT_BASE_URL + clean_api_base: Final = (api_base or self.DEFAULT_BASE_URL).rstrip("/") - api_base = api_base.rstrip("/") + if not clean_api_base.endswith("/chat/completions"): + return f"{clean_api_base}/chat/completions" - if not api_base.endswith("/chat/completions"): - api_base = f"{api_base}/chat/completions" - - return api_base + return clean_api_base diff --git a/litellm/llms/moonshot/chat/transformation.py b/litellm/llms/moonshot/chat/transformation.py index 7f2c96e121b..a6b546ae4e2 100644 --- a/litellm/llms/moonshot/chat/transformation.py +++ b/litellm/llms/moonshot/chat/transformation.py @@ -86,15 +86,12 @@ class MoonshotChatConfig(OpenAIGPTConfig): """ If api_base is not provided, use the default Moonshot AI /chat/completions endpoint. """ - if not api_base: - api_base = "https://api.moonshot.ai/v1" + clean_api_base: Final = (api_base or "https://api.moonshot.ai/v1").rstrip("/") - api_base = api_base.rstrip("/") + if not clean_api_base.endswith("/chat/completions"): + return f"{clean_api_base}/chat/completions" - if not api_base.endswith("/chat/completions"): - api_base = f"{api_base}/chat/completions" - - return api_base + return clean_api_base def get_supported_openai_params(self, model: str) -> list: """ diff --git a/litellm/llms/ollama/chat/transformation.py b/litellm/llms/ollama/chat/transformation.py index 851ed3cc5f3..862db62d9df 100644 --- a/litellm/llms/ollama/chat/transformation.py +++ b/litellm/llms/ollama/chat/transformation.py @@ -218,15 +218,10 @@ class OllamaChatConfig(BaseConfig): Some providers need `model` in `api_base` """ - 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: - url = f"{api_base}/api/chat" - - return url + clean_api_base: Final = (api_base or "http://localhost:11434").rstrip("/") + if clean_api_base.endswith("/api/chat"): + return clean_api_base + return f"{clean_api_base}/api/chat" def transform_request( self, diff --git a/litellm/llms/ollama/completion/transformation.py b/litellm/llms/ollama/completion/transformation.py index 5c3c2be8fbf..7e5b24f9db8 100644 --- a/litellm/llms/ollama/completion/transformation.py +++ b/litellm/llms/ollama/completion/transformation.py @@ -468,15 +468,10 @@ class OllamaConfig(BaseConfig): Some providers need `model` in `api_base` """ - 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: - url = f"{api_base}/api/generate" - - return url + clean_api_base: Final = (api_base or "http://localhost:11434").rstrip("/") + if clean_api_base.endswith("/api/generate"): + return clean_api_base + return f"{clean_api_base}/api/generate" def get_model_response_iterator( self, diff --git a/litellm/llms/perplexity/embedding/transformation.py b/litellm/llms/perplexity/embedding/transformation.py index bc011978653..0e6de725c37 100644 --- a/litellm/llms/perplexity/embedding/transformation.py +++ b/litellm/llms/perplexity/embedding/transformation.py @@ -61,10 +61,10 @@ 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 + clean_api_base: Final = api_base.rstrip("/") + if not clean_api_base.endswith("/embeddings"): + return f"{clean_api_base}/v1/embeddings" + return clean_api_base return "https://api.perplexity.ai/v1/embeddings" def get_supported_openai_params(self, model: str) -> list: diff --git a/litellm/llms/topaz/image_variations/transformation.py b/litellm/llms/topaz/image_variations/transformation.py index 441f751cc36..8d03b73e94e 100644 --- a/litellm/llms/topaz/image_variations/transformation.py +++ b/litellm/llms/topaz/image_variations/transformation.py @@ -39,8 +39,8 @@ class TopazImageVariationConfig(TopazModelInfo, BaseImageVariationConfig): litellm_params: dict, stream: bool | None = None, ) -> str: - api_base = (api_base or "https://api.topazlabs.com").rstrip("/") - return f"{api_base}/image/v1/enhance" + clean_api_base: Final = (api_base or "https://api.topazlabs.com").rstrip("/") + return f"{clean_api_base}/image/v1/enhance" def map_openai_params( self, diff --git a/litellm/llms/voyage/embedding/transformation.py b/litellm/llms/voyage/embedding/transformation.py index e08ba9a9e7e..8f52c1bb773 100644 --- a/litellm/llms/voyage/embedding/transformation.py +++ b/litellm/llms/voyage/embedding/transformation.py @@ -46,10 +46,10 @@ 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 + clean_api_base: Final = api_base.rstrip("/") + if not clean_api_base.endswith("/embeddings"): + return f"{clean_api_base}/embeddings" + return clean_api_base return "https://api.voyageai.com/v1/embeddings" def get_supported_openai_params(self, model: str) -> list: diff --git a/litellm/llms/voyage/embedding/transformation_contextual.py b/litellm/llms/voyage/embedding/transformation_contextual.py index 1a5001bd7c1..4c97301fe29 100644 --- a/litellm/llms/voyage/embedding/transformation_contextual.py +++ b/litellm/llms/voyage/embedding/transformation_contextual.py @@ -55,10 +55,10 @@ 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 + clean_api_base: Final = api_base.rstrip("/") + if not clean_api_base.endswith("/contextualizedembeddings"): + return f"{clean_api_base}/contextualizedembeddings" + return clean_api_base return "https://api.voyageai.com/v1/contextualizedembeddings" def get_supported_openai_params(self, model: str) -> list: # mutable-ok: base class signature diff --git a/litellm/llms/voyage/embedding/transformation_multimodal.py b/litellm/llms/voyage/embedding/transformation_multimodal.py index 7bd9f2a5f68..ba5658e0568 100644 --- a/litellm/llms/voyage/embedding/transformation_multimodal.py +++ b/litellm/llms/voyage/embedding/transformation_multimodal.py @@ -55,10 +55,10 @@ 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 + clean_api_base: Final = api_base.rstrip("/") + if not clean_api_base.endswith("/multimodalembeddings"): + return f"{clean_api_base}/multimodalembeddings" + return clean_api_base return "https://api.voyageai.com/v1/multimodalembeddings" def get_supported_openai_params(self, model: str) -> list: diff --git a/tests/llm_translation/test_provider_url_trailing_slash.py b/tests/llm_translation/test_provider_url_trailing_slash.py deleted file mode 100644 index 1864a94a28f..00000000000 --- a/tests/llm_translation/test_provider_url_trailing_slash.py +++ /dev/null @@ -1,126 +0,0 @@ -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 diff --git a/tests/test_litellm/llms/test_provider_url_trailing_slash.py b/tests/test_litellm/llms/test_provider_url_trailing_slash.py new file mode 100644 index 00000000000..ce98fc26f05 --- /dev/null +++ b/tests/test_litellm/llms/test_provider_url_trailing_slash.py @@ -0,0 +1,242 @@ +from litellm.llms.deepseek.chat.transformation import DeepSeekChatConfig +from litellm.llms.heroku.chat.transformation import HerokuChatConfig +from litellm.llms.jina_ai.embedding.transformation import JinaAIEmbeddingConfig +from litellm.llms.modelscope.chat.transformation import ModelScopeChatConfig +from litellm.llms.moonshot.chat.transformation import MoonshotChatConfig +from litellm.llms.ollama.chat.transformation import OllamaChatConfig +from litellm.llms.ollama.completion.transformation import OllamaConfig +from litellm.llms.perplexity.embedding.transformation import PerplexityEmbeddingConfig +from litellm.llms.topaz.image_variations.transformation import TopazImageVariationConfig +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 + + +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_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