From 3c1058ef7c8367497acd8a7f4e70b98eff3ff430 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 17 Apr 2025 17:27:47 +0000 Subject: [PATCH] Fix output_json with custom OpenAI APIs by using PARALLEL_TOOLS mode Co-Authored-By: Joe Moura --- src/crewai/utilities/internal_instructor.py | 13 +++- tests/utilities/test_internal_instructor.py | 72 +++++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 tests/utilities/test_internal_instructor.py diff --git a/src/crewai/utilities/internal_instructor.py b/src/crewai/utilities/internal_instructor.py index e9401c778..a147f1591 100644 --- a/src/crewai/utilities/internal_instructor.py +++ b/src/crewai/utilities/internal_instructor.py @@ -29,7 +29,14 @@ class InternalInstructor: import instructor from litellm import completion - self._client = instructor.from_litellm(completion) + is_custom_openai = getattr(self.llm, 'model', '').startswith('custom_openai/') + + mode = instructor.Mode.PARALLEL_TOOLS if is_custom_openai else instructor.Mode.TOOLS + + self._client = instructor.from_litellm( + completion, + mode=mode, + ) def to_json(self): model = self.to_pydantic() @@ -40,4 +47,8 @@ class InternalInstructor: model = self._client.chat.completions.create( model=self.llm.model, response_model=self.model, messages=messages ) + + if isinstance(model, list) and len(model) > 0: + return model[0] # Return the first model from the list + return model diff --git a/tests/utilities/test_internal_instructor.py b/tests/utilities/test_internal_instructor.py new file mode 100644 index 000000000..b180e2948 --- /dev/null +++ b/tests/utilities/test_internal_instructor.py @@ -0,0 +1,72 @@ +import unittest +from unittest.mock import MagicMock, patch + +import pytest +from pydantic import BaseModel + +from crewai.utilities.internal_instructor import InternalInstructor + + +class TestOutput(BaseModel): + value: str + + +class TestInternalInstructor(unittest.TestCase): + @patch("instructor.from_litellm") + def test_tools_mode_for_regular_models(self, mock_from_litellm): + mock_llm = MagicMock() + mock_llm.model = "gpt-4o" + mock_instructor = MagicMock() + mock_from_litellm.return_value = mock_instructor + + instructor = InternalInstructor( + content="Test content", + model=TestOutput, + llm=mock_llm + ) + + import instructor + mock_from_litellm.assert_called_once_with( + unittest.mock.ANY, + mode=instructor.Mode.TOOLS + ) + + @patch("instructor.from_litellm") + def test_parallel_tools_mode_for_custom_openai(self, mock_from_litellm): + mock_llm = MagicMock() + mock_llm.model = "custom_openai/some-model" + mock_instructor = MagicMock() + mock_from_litellm.return_value = mock_instructor + + instructor = InternalInstructor( + content="Test content", + model=TestOutput, + llm=mock_llm + ) + + import instructor + mock_from_litellm.assert_called_once_with( + unittest.mock.ANY, + mode=instructor.Mode.PARALLEL_TOOLS + ) + + @patch("instructor.from_litellm") + def test_handling_list_response_in_to_pydantic(self, mock_from_litellm): + mock_llm = MagicMock() + mock_llm.model = "custom_openai/some-model" + mock_instructor = MagicMock() + mock_chat = MagicMock() + mock_instructor.chat.completions.create.return_value = [ + TestOutput(value="test value") + ] + mock_from_litellm.return_value = mock_instructor + + instructor = InternalInstructor( + content="Test content", + model=TestOutput, + llm=mock_llm + ) + result = instructor.to_pydantic() + + assert isinstance(result, TestOutput) + assert result.value == "test value"