diff --git a/keep/providers/deepseek_provider/deepseek_provider.py b/keep/providers/deepseek_provider/deepseek_provider.py index 4e697bdddb..aa0ff5e087 100644 --- a/keep/providers/deepseek_provider/deepseek_provider.py +++ b/keep/providers/deepseek_provider/deepseek_provider.py @@ -80,7 +80,13 @@ def _query( max_tokens=max_tokens, response_format=structured_output_format, ) - response = response.choices[0].message.content + # An OpenAI-compatible backend can return HTTP 200 with an empty + # choices list (e.g. a content-filter block), so response.choices[0] + # would raise IndexError. Degrade to an empty string instead. + try: + response = response.choices[0].message.content + except (IndexError, AttributeError): + response = "" try: response = json.loads(response) except Exception: diff --git a/keep/providers/openai_provider/openai_provider.py b/keep/providers/openai_provider/openai_provider.py index be7460b15f..151bf98a07 100644 --- a/keep/providers/openai_provider/openai_provider.py +++ b/keep/providers/openai_provider/openai_provider.py @@ -66,7 +66,13 @@ def _query( max_tokens=max_tokens, response_format=structured_output_format, ) - response = response.choices[0].message.content + # An OpenAI-compatible backend can return HTTP 200 with an empty + # choices list (e.g. a content-filter block), so response.choices[0] + # would raise IndexError. Degrade to an empty string instead. + try: + response = response.choices[0].message.content + except (IndexError, AttributeError): + response = "" try: response = json.loads(response) except Exception: diff --git a/tests/providers/deepseek_provider/test_deepseek_response_parsing.py b/tests/providers/deepseek_provider/test_deepseek_response_parsing.py new file mode 100644 index 0000000000..98e7acfc66 --- /dev/null +++ b/tests/providers/deepseek_provider/test_deepseek_response_parsing.py @@ -0,0 +1,53 @@ +"""Tests for how the DeepSeek provider parses a chat completion response. + +DeepSeek is accessed through the OpenAI-compatible SDK, and an +OpenAI-compatible backend can return HTTP 200 with an empty ``choices`` list +(e.g. a content-filter block). The provider must not crash when it indexes +into ``choices[0]`` in that case. +""" + +from unittest.mock import MagicMock, patch + +from keep.contextmanager.contextmanager import ContextManager +from keep.providers.deepseek_provider.deepseek_provider import DeepseekProvider +from keep.providers.models.provider_config import ProviderConfig + + +def _build_provider() -> DeepseekProvider: + config = ProviderConfig( + description="DeepSeek Provider", + authentication={"api_key": "test-key"}, + ) + return DeepseekProvider(ContextManager(tenant_id="test"), "deepseek-test", config) + + +def _completion(choices): + response = MagicMock() + response.choices = choices + return response + + +def test_empty_choices_does_not_crash(): + provider = _build_provider() + with patch( + "keep.providers.deepseek_provider.deepseek_provider.OpenAI" + ) as mock_openai: + mock_openai.return_value.chat.completions.create.return_value = _completion( + [] + ) + assert provider._query(prompt="hi")["response"] == "" + + +def test_normal_response_is_returned(): + provider = _build_provider() + message = MagicMock() + message.content = "hello" + choice = MagicMock() + choice.message = message + with patch( + "keep.providers.deepseek_provider.deepseek_provider.OpenAI" + ) as mock_openai: + mock_openai.return_value.chat.completions.create.return_value = _completion( + [choice] + ) + assert provider._query(prompt="hi")["response"] == "hello" diff --git a/tests/providers/openai_provider/test_openai_response_parsing.py b/tests/providers/openai_provider/test_openai_response_parsing.py new file mode 100644 index 0000000000..8cc8617f5d --- /dev/null +++ b/tests/providers/openai_provider/test_openai_response_parsing.py @@ -0,0 +1,52 @@ +"""Tests for how the OpenAI provider parses a chat completion response. + +An OpenAI-compatible backend can return HTTP 200 with an empty ``choices`` +list (e.g. a content-filter block). The SDK does not raise in that case, so +the provider must not crash when it indexes into ``choices[0]``. +""" + +from unittest.mock import MagicMock, patch + +from keep.contextmanager.contextmanager import ContextManager +from keep.providers.openai_provider.openai_provider import OpenaiProvider +from keep.providers.models.provider_config import ProviderConfig + + +def _build_provider() -> OpenaiProvider: + config = ProviderConfig( + description="OpenAI Provider", + authentication={"api_key": "test-key"}, + ) + return OpenaiProvider(ContextManager(tenant_id="test"), "openai-test", config) + + +def _completion(choices): + response = MagicMock() + response.choices = choices + return response + + +def test_empty_choices_does_not_crash(): + provider = _build_provider() + with patch( + "keep.providers.openai_provider.openai_provider.OpenAI" + ) as mock_openai: + mock_openai.return_value.chat.completions.create.return_value = _completion( + [] + ) + assert provider._query(prompt="hi")["response"] == "" + + +def test_normal_response_is_returned(): + provider = _build_provider() + message = MagicMock() + message.content = "hello" + choice = MagicMock() + choice.message = message + with patch( + "keep.providers.openai_provider.openai_provider.OpenAI" + ) as mock_openai: + mock_openai.return_value.chat.completions.create.return_value = _completion( + [choice] + ) + assert provider._query(prompt="hi")["response"] == "hello"