diff --git a/litellm/llms/openai/openai.py b/litellm/llms/openai/openai.py index 194f29648c4..d3f5dc13542 100644 --- a/litellm/llms/openai/openai.py +++ b/litellm/llms/openai/openai.py @@ -1201,17 +1201,35 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): Helper to: - call embeddings.create.with_raw_response when litellm.return_response_headers is True - call embeddings.create by default + + Returns (headers, response_dict) where response_dict is always a dict. """ try: raw_response = await openai_aclient.embeddings.with_raw_response.create( **data, timeout=timeout ) # type: ignore headers = dict(raw_response.headers) - response = raw_response.parse() - return headers, response + parsed = raw_response.parse() + return headers, self._normalize_embedding_response(parsed, data) except Exception as e: raise e + def _normalize_embedding_response( + self, + response: Any, + request_data: dict, + ) -> dict: + if hasattr(response, "model_dump"): + return response.model_dump() + if isinstance(response, dict): + return response + return { + "object": "list", + "data": list(response) if isinstance(response, list) else [], + "model": request_data.get("model", ""), + "usage": {"prompt_tokens": 0, "total_tokens": 0}, + } + @track_llm_api_timing() def make_sync_openai_embedding_request( self, @@ -1224,6 +1242,8 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): Helper to: - call embeddings.create.with_raw_response when litellm.return_response_headers is True - call embeddings.create by default + + Returns (headers, response_dict) where response_dict is always a dict. """ try: raw_response = openai_client.embeddings.with_raw_response.create( @@ -1231,8 +1251,8 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): ) # type: ignore headers = dict(raw_response.headers) - response = raw_response.parse() - return headers, response + parsed = raw_response.parse() + return headers, self._normalize_embedding_response(parsed, data) except Exception as e: raise e @@ -1266,16 +1286,15 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): logging_obj=logging_obj, ) logging_obj.model_call_details["response_headers"] = headers - stringified_response = response.model_dump() ## LOGGING logging_obj.post_call( input=input, api_key=api_key, additional_args={"complete_input_dict": data}, - original_response=stringified_response, + original_response=response, ) returned_response: EmbeddingResponse = convert_to_model_response_object( - response_object=stringified_response, + response_object=response, model_response_object=model_response, response_type="embedding", _response_headers=headers, @@ -1377,7 +1396,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): original_response=sync_embedding_response, ) response: EmbeddingResponse = convert_to_model_response_object( - response_object=sync_embedding_response.model_dump(), + response_object=sync_embedding_response, model_response_object=model_response, _response_headers=headers, response_type="embedding", diff --git a/litellm/llms/openai_like/embedding/handler.py b/litellm/llms/openai_like/embedding/handler.py index e3884fa56d7..33ec4a69226 100644 --- a/litellm/llms/openai_like/embedding/handler.py +++ b/litellm/llms/openai_like/embedding/handler.py @@ -13,6 +13,9 @@ from litellm.llms.custom_httpx.http_handler import ( HTTPHandler, get_async_httpx_client, ) +from litellm.litellm_core_utils.llm_response_utils.convert_dict_to_response import ( + convert_to_model_response_object, +) from litellm.types.utils import EmbeddingResponse from ..common_utils import OpenAILikeBase, OpenAILikeError @@ -72,7 +75,22 @@ class OpenAILikeEmbeddingHandler(OpenAILikeBase): additional_args={"complete_input_dict": data}, original_response=response_json, ) - return EmbeddingResponse(**response_json) + if not isinstance(response_json, dict): + raise OpenAILikeError( + status_code=500, + message=( + "Embedding response is not a mapping. " + "If you are using an OpenAI-compatible server, " + "make sure the api_base includes the API prefix (e.g. /v1). " + f"Received type: {type(response_json).__name__}. " + f"Response: {str(response_json)[:500]}" + ), + ) + return convert_to_model_response_object( + response_object=response_json, + model_response_object=model_response, + response_type="embedding", + ) except Exception as e: ## LOGGING logging_obj.post_call( @@ -153,4 +171,19 @@ class OpenAILikeEmbeddingHandler(OpenAILikeBase): original_response=response_json, ) - return litellm.EmbeddingResponse(**response_json) + if not isinstance(response_json, dict): + raise OpenAILikeError( + status_code=500, + message=( + "Embedding response is not a mapping. " + "If you are using an OpenAI-compatible server, " + "make sure the api_base includes the API prefix (e.g. /v1). " + f"Received type: {type(response_json).__name__}. " + f"Response: {str(response_json)[:500]}" + ), + ) + return convert_to_model_response_object( + response_object=response_json, + model_response_object=model_response or EmbeddingResponse(), + response_type="embedding", + ) diff --git a/tests/test_litellm/llms/openai_like/embedding/test_openai_like_embedding.py b/tests/test_litellm/llms/openai_like/embedding/test_openai_like_embedding.py index 89348d505bb..69c60b856d8 100644 --- a/tests/test_litellm/llms/openai_like/embedding/test_openai_like_embedding.py +++ b/tests/test_litellm/llms/openai_like/embedding/test_openai_like_embedding.py @@ -7,6 +7,7 @@ from unittest.mock import MagicMock, Mock, patch import pytest +from litellm.llms.openai_like.common_utils import OpenAILikeError from litellm.llms.openai_like.embedding.handler import OpenAILikeEmbeddingHandler from litellm.types.utils import EmbeddingResponse @@ -346,3 +347,72 @@ class TestOpenAILikeEmbeddingHandler: assert sent_data["model"] == "test-model" assert sent_data["input"] == ["test input"] assert "encoding_format" not in sent_data + + def test_embedding_response_list_raises_type_error(self): + handler = OpenAILikeEmbeddingHandler() + + mock_client = MagicMock() + mock_response = Mock() + mock_response.json.return_value = [ + {"object": "embedding", "embedding": [0.1, 0.2, 0.3], "index": 0} + ] + mock_response.raise_for_status = Mock() + mock_client.post.return_value = mock_response + + mock_logging = MagicMock() + + with patch.object( + handler, + "_validate_environment", + return_value=("http://test.com/v1/embeddings", {}), + ): + with pytest.raises(OpenAILikeError) as exc_info: + handler.embedding( + model="test-model", + input=["test input"], + timeout=60.0, + logging_obj=mock_logging, + api_key="test-key", + api_base="http://test.com/v1", + optional_params={}, + client=mock_client, + ) + assert "not a mapping" in str(exc_info.value.message) + assert "api_base includes the API prefix" in str(exc_info.value.message) + + def test_embedding_response_dict_uses_convert_to_model_response_object(self): + handler = OpenAILikeEmbeddingHandler() + + mock_client = MagicMock() + mock_response = Mock() + mock_response.json.return_value = { + "object": "list", + "data": [{"object": "embedding", "embedding": [0.1, 0.2, 0.3], "index": 0}], + "model": "test-model", + "usage": {"prompt_tokens": 5, "total_tokens": 5}, + } + mock_response.raise_for_status = Mock() + mock_client.post.return_value = mock_response + + mock_logging = MagicMock() + + with patch.object( + handler, + "_validate_environment", + return_value=("http://test.com/v1/embeddings", {}), + ): + response = handler.embedding( + model="test-model", + input=["test input"], + timeout=60.0, + logging_obj=mock_logging, + api_key="test-key", + api_base="http://test.com/v1", + optional_params={}, + client=mock_client, + ) + + assert response.model == "test-model" + assert response.object == "list" + assert len(response.data) == 1 + assert response.data[0]["embedding"] == [0.1, 0.2, 0.3]