Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions backend/utils/llm_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,10 @@ def call_llm_for_system_prompt(
if delta is None:
logger.debug("Skipping LLM stream chunk without delta")
continue

reasoning_content = getattr(delta, "reasoning_content", None)

reasoning_content = getattr(delta, "reasoning", None)
if reasoning_content is None:
reasoning_content = getattr(delta, "reasoning_content", None)
new_token = getattr(delta, "content", None)

# Note: reasoning_content is separate metadata and doesn't affect content filtering
Expand Down
7 changes: 4 additions & 3 deletions sdk/nexent/core/models/openai_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,9 +391,10 @@ def __call__(self, messages: List[Dict[str, Any]], stop_sequences: Optional[List
if chunk_finish_reason is not None:
finish_reason = str(chunk_finish_reason)

new_token = chunk.choices[0].delta.content
reasoning_content = getattr(
chunk.choices[0].delta, 'reasoning_content', None)
new_token = getattr(chunk.choices[0].delta, "content", None)
reasoning_content = getattr(chunk.choices[0].delta, "reasoning", None)
if reasoning_content is None:
reasoning_content = getattr(chunk.choices[0].delta, "reasoning_content", None)

# Handle reasoning_content if it exists and is not null
if reasoning_content is not None:
Expand Down
35 changes: 35 additions & 0 deletions test/backend/utils/test_llm_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,7 @@ def mock_callback(text):

def test_call_llm_for_system_prompt_with_reasoning_content(self, mocker: MockFixture):
"""Test call_llm_for_system_prompt with reasoning_content"""
mock_logger = mocker.patch('backend.utils.llm_utils.logger')
mock_get_model_by_id = mocker.patch('backend.utils.llm_utils.get_model_by_model_id')
mock_adapter = mocker.patch('backend.utils.llm_utils.get_llm_adapter_from_config')

Expand All @@ -603,6 +604,7 @@ def test_call_llm_for_system_prompt_with_reasoning_content(self, mocker: MockFix
mock_chunk = MagicMock()
mock_chunk.choices = [MagicMock()]
mock_chunk.choices[0].delta.content = "Generated prompt"
mock_chunk.choices[0].delta.reasoning = None
mock_chunk.choices[0].delta.reasoning_content = "Some reasoning"

mock_llm_instance.client = MagicMock()
Expand All @@ -616,6 +618,39 @@ def test_call_llm_for_system_prompt_with_reasoning_content(self, mocker: MockFix
)

assert result == "Generated prompt"
mock_logger.debug.assert_any_call(
"Received reasoning_content (metadata only, not filtering content)"
)

def test_call_llm_for_system_prompt_with_reasoning(self, mocker: MockFixture):
"""Test call_llm_for_system_prompt with the alternate reasoning field."""
mock_logger = mocker.patch('backend.utils.llm_utils.logger')
mock_get_model_by_id = mocker.patch('backend.utils.llm_utils.get_model_by_model_id')
mock_adapter = mocker.patch('backend.utils.llm_utils.get_llm_adapter_from_config')

mock_get_model_by_id.return_value = {"base_url": "http://example.com", "api_key": "fake-key"}

mock_llm_instance = mock_adapter.return_value
mock_chunk = MagicMock()
mock_chunk.choices = [MagicMock()]
mock_chunk.choices[0].delta.content = "Generated prompt"
mock_chunk.choices[0].delta.reasoning = "Some reasoning"
mock_chunk.choices[0].delta.reasoning_content = None

mock_llm_instance.client = MagicMock()
mock_llm_instance.client.chat.completions.create.return_value = [mock_chunk]
mock_llm_instance._prepare_completion_kwargs.return_value = {}

result = call_llm_for_system_prompt(
1,
"user prompt",
"system prompt",
)

assert result == "Generated prompt"
mock_logger.debug.assert_any_call(
"Received reasoning_content (metadata only, not filtering content)"
)

def test_call_llm_for_system_prompt_multiple_chunks(self, mocker: MockFixture):
"""Test call_llm_for_system_prompt with multiple chunks"""
Expand Down
52 changes: 52 additions & 0 deletions test/sdk/core/models/test_openai_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -790,12 +790,14 @@ def test_call_with_reasoning_content(openai_model_instance):
mock_chunk1.choices = [MagicMock()]
mock_chunk1.choices[0].delta.content = "Let me think about this"
mock_chunk1.choices[0].delta.role = "assistant"
mock_chunk1.choices[0].delta.reasoning = None
mock_chunk1.choices[0].delta.reasoning_content = "This is a reasoning step"

mock_chunk2 = MagicMock()
mock_chunk2.choices = [MagicMock()]
mock_chunk2.choices[0].delta.content = "Response"
mock_chunk2.choices[0].delta.role = None
mock_chunk2.choices[0].delta.reasoning = None
mock_chunk2.choices[0].delta.reasoning_content = None
mock_chunk2.usage = MagicMock()
mock_chunk2.usage.prompt_tokens = 5
Expand Down Expand Up @@ -828,6 +830,36 @@ def test_call_with_reasoning_content(openai_model_instance):
"Response")


def test_call_with_reasoning_field(openai_model_instance):
"""Test __call__ handles providers that stream reasoning in the reasoning field."""
messages = [{"role": "user", "content": [{"text": "Hello"}]}]

mock_chunk = MagicMock()
mock_chunk.choices = [MagicMock()]
mock_chunk.choices[0].delta.content = "Response"
mock_chunk.choices[0].delta.role = "assistant"
mock_chunk.choices[0].delta.reasoning = "Alternate reasoning field"
mock_chunk.usage = MagicMock()
mock_chunk.usage.prompt_tokens = 5
mock_chunk.usage.total_tokens = 8

mock_result_message = MagicMock()
mock_result_message.raw = [mock_chunk]
mock_result_message.role = MagicMock()

with patch.object(openai_model_instance, "_prepare_completion_kwargs", return_value={}), \
patch.object(mock_models_module.ChatMessage, "from_dict", return_value=mock_result_message):
openai_model_instance.client.chat.completions.create.return_value = [mock_chunk]

result = openai_model_instance.__call__(messages)

assert result == mock_result_message
openai_model_instance.observer.add_model_reasoning_content.assert_called_once_with(
"Alternate reasoning field"
)
openai_model_instance.observer.add_model_new_token.assert_called_once_with("Response")


def test_call_with_multiple_reasoning_content_chunks(openai_model_instance):
"""Test __call__ method handles multiple chunks with reasoning_content"""

Expand All @@ -838,18 +870,21 @@ def test_call_with_multiple_reasoning_content_chunks(openai_model_instance):
mock_chunk1.choices = [MagicMock()]
mock_chunk1.choices[0].delta.content = "Let me"
mock_chunk1.choices[0].delta.role = "assistant"
mock_chunk1.choices[0].delta.reasoning = None
mock_chunk1.choices[0].delta.reasoning_content = "First reasoning step"

mock_chunk2 = MagicMock()
mock_chunk2.choices = [MagicMock()]
mock_chunk2.choices[0].delta.content = " think"
mock_chunk2.choices[0].delta.role = None
mock_chunk2.choices[0].delta.reasoning = None
mock_chunk2.choices[0].delta.reasoning_content = "Second reasoning step"

mock_chunk3 = MagicMock()
mock_chunk3.choices = [MagicMock()]
mock_chunk3.choices[0].delta.content = " about this"
mock_chunk3.choices[0].delta.role = None
mock_chunk3.choices[0].delta.reasoning = None
mock_chunk3.choices[0].delta.reasoning_content = None
mock_chunk3.usage = MagicMock()
mock_chunk3.usage.prompt_tokens = 5
Expand Down Expand Up @@ -896,12 +931,14 @@ def test_call_with_reasoning_content_only(openai_model_instance):
mock_chunk1.choices = [MagicMock()]
mock_chunk1.choices[0].delta.content = None
mock_chunk1.choices[0].delta.role = "assistant"
mock_chunk1.choices[0].delta.reasoning = None
mock_chunk1.choices[0].delta.reasoning_content = "Pure reasoning content"

mock_chunk2 = MagicMock()
mock_chunk2.choices = [MagicMock()]
mock_chunk2.choices[0].delta.content = "Final response"
mock_chunk2.choices[0].delta.role = None
mock_chunk2.choices[0].delta.reasoning = None
mock_chunk2.choices[0].delta.reasoning_content = None
mock_chunk2.usage = MagicMock()
mock_chunk2.usage.prompt_tokens = 5
Expand Down Expand Up @@ -942,6 +979,7 @@ def test_call_rejects_reasoning_only_response_and_records_diagnostics(
reasoning_chunk.choices = [MagicMock()]
reasoning_chunk.choices[0].delta.content = None
reasoning_chunk.choices[0].delta.role = "assistant"
reasoning_chunk.choices[0].delta.reasoning = None
reasoning_chunk.choices[0].delta.reasoning_content = "Internal reasoning"
reasoning_chunk.choices[0].finish_reason = None
reasoning_chunk.usage = None
Expand All @@ -950,6 +988,7 @@ def test_call_rejects_reasoning_only_response_and_records_diagnostics(
final_chunk.choices = [MagicMock()]
final_chunk.choices[0].delta.content = None
final_chunk.choices[0].delta.role = None
final_chunk.choices[0].delta.reasoning = None
final_chunk.choices[0].delta.reasoning_content = None
final_chunk.choices[0].finish_reason = "length"
final_chunk.usage = MagicMock(prompt_tokens=10, completion_tokens=20)
Expand Down Expand Up @@ -985,6 +1024,7 @@ def test_call_with_reasoning_content_and_content_together(openai_model_instance)
mock_chunk.choices = [MagicMock()]
mock_chunk.choices[0].delta.content = "Response text"
mock_chunk.choices[0].delta.role = "assistant"
mock_chunk.choices[0].delta.reasoning = None
mock_chunk.choices[0].delta.reasoning_content = "Reasoning alongside content"
mock_chunk.usage = MagicMock()
mock_chunk.usage.prompt_tokens = 5
Expand Down Expand Up @@ -1067,18 +1107,21 @@ def test_call_with_monitoring_and_token_tracker(openai_model_instance):
mock_chunk1.choices = [MagicMock()]
mock_chunk1.choices[0].delta.content = "Hello"
mock_chunk1.choices[0].delta.role = "assistant"
mock_chunk1.choices[0].delta.reasoning = None
mock_chunk1.choices[0].delta.reasoning_content = None

mock_chunk2 = MagicMock()
mock_chunk2.choices = [MagicMock()]
mock_chunk2.choices[0].delta.content = " world"
mock_chunk2.choices[0].delta.role = None
mock_chunk2.choices[0].delta.reasoning = None
mock_chunk2.choices[0].delta.reasoning_content = None

mock_chunk3 = MagicMock()
mock_chunk3.choices = [MagicMock()]
mock_chunk3.choices[0].delta.content = None
mock_chunk3.choices[0].delta.role = None
mock_chunk3.choices[0].delta.reasoning = None
mock_chunk3.choices[0].delta.reasoning_content = None
mock_chunk3.usage = MagicMock()
mock_chunk3.usage.prompt_tokens = 10
Expand Down Expand Up @@ -1126,12 +1169,14 @@ def test_call_with_token_tracker_on_reasoning_content(openai_model_instance):
mock_chunk1.choices = [MagicMock()]
mock_chunk1.choices[0].delta.content = None
mock_chunk1.choices[0].delta.role = "assistant"
mock_chunk1.choices[0].delta.reasoning = None
mock_chunk1.choices[0].delta.reasoning_content = "Thinking..."

mock_chunk2 = MagicMock()
mock_chunk2.choices = [MagicMock()]
mock_chunk2.choices[0].delta.content = "Response"
mock_chunk2.choices[0].delta.role = None
mock_chunk2.choices[0].delta.reasoning = None
mock_chunk2.choices[0].delta.reasoning_content = None
mock_chunk2.usage = MagicMock()
mock_chunk2.usage.prompt_tokens = 5
Expand Down Expand Up @@ -1170,6 +1215,7 @@ def test_call_with_stop_event_and_token_tracker(openai_model_instance):
mock_chunk.choices = [MagicMock()]
mock_chunk.choices[0].delta.content = "Response"
mock_chunk.choices[0].delta.role = "assistant"
mock_chunk.choices[0].delta.reasoning = None
mock_chunk.choices[0].delta.reasoning_content = None

with patch.object(openai_model_instance, "_prepare_completion_kwargs", return_value={}):
Expand Down Expand Up @@ -1753,6 +1799,7 @@ def test_call_token_estimation_with_list_content(openai_model_instance):
mock_chunk.choices = [MagicMock()]
mock_chunk.choices[0].delta.content = "Response"
mock_chunk.choices[0].delta.role = "assistant"
mock_chunk.choices[0].delta.reasoning = None
mock_chunk.choices[0].delta.reasoning_content = None
mock_chunk.usage = None # No usage info to trigger token estimation

Expand Down Expand Up @@ -1801,6 +1848,7 @@ def test_prompt_cache_plan_records_unknown_capability_without_payload_directive(
mock_chunk.choices = [MagicMock()]
mock_chunk.choices[0].delta.content = "Response"
mock_chunk.choices[0].delta.role = "assistant"
mock_chunk.choices[0].delta.reasoning = None
mock_chunk.choices[0].delta.reasoning_content = None
mock_chunk.usage = MagicMock()
mock_chunk.usage.prompt_tokens = 10
Expand All @@ -1827,6 +1875,7 @@ def test_prompt_cache_usage_extracts_openai_cached_tokens(openai_model_instance)
mock_chunk.choices = [MagicMock()]
mock_chunk.choices[0].delta.content = "Response"
mock_chunk.choices[0].delta.role = "assistant"
mock_chunk.choices[0].delta.reasoning = None
mock_chunk.choices[0].delta.reasoning_content = None
mock_chunk.usage = MagicMock()
mock_chunk.usage.prompt_tokens = 100
Expand Down Expand Up @@ -1856,6 +1905,7 @@ def test_provider_adapter_preserves_context_manager_tool_order(openai_model_inst
mock_chunk.choices = [MagicMock()]
mock_chunk.choices[0].delta.content = "ok"
mock_chunk.choices[0].delta.role = "assistant"
mock_chunk.choices[0].delta.reasoning = None
mock_chunk.choices[0].delta.reasoning_content = None
mock_chunk.choices[0].finish_reason = "stop"
mock_chunk.usage = MagicMock(prompt_tokens=1, completion_tokens=1)
Expand Down Expand Up @@ -1891,6 +1941,7 @@ def _make_content_chunk(content: str = "hi"):
chunk.choices = [MagicMock()]
chunk.choices[0].delta.content = content
chunk.choices[0].delta.role = "assistant"
chunk.choices[0].delta.reasoning = None
chunk.choices[0].delta.reasoning_content = None
chunk.usage = MagicMock()
chunk.usage.prompt_tokens = 1
Expand Down Expand Up @@ -2274,6 +2325,7 @@ def test_streaming_without_usage_falls_back_to_input_text(openai_model_instance)
clean_delta = types.SimpleNamespace()
clean_delta.content = "ok"
clean_delta.role = "assistant"
clean_delta.reasoning = None
clean_delta.reasoning_content = None
clean_choice.delta = clean_delta
clean_chunk.choices = [clean_choice]
Expand Down
Loading