Skip to content
Open
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
7 changes: 4 additions & 3 deletions python/dify_plugin/interfaces/model/large_language_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -526,16 +526,17 @@ def _code_block_mode_stream_processor_with_backtick(

def _wrap_thinking_by_reasoning_content(self, delta: dict, is_reasoning: bool) -> tuple[str, bool]:
"""
If the reasoning response is from delta.get("reasoning_content"), we wrap
it with HTML think tag.
If the reasoning response is from delta.get("reasoning_content") or delta.get("reasoning"),
we wrap it with HTML think tag.

:param delta: delta dictionary from LLM streaming response
:param is_reasoning: is reasoning
:return: tuple of (processed_content, is_reasoning)
"""

content = delta.get("content") or ""
reasoning_content = delta.get("reasoning_content")
reasoning_content = delta.get("reasoning_content") or delta.get("reasoning")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This change, combined with the existing logic, could lead to a TypeError if delta.get('reasoning_content') or delta.get('reasoning') returns a truthy non-string value (e.g., a dictionary or a list). The string concatenation on line 542 would fail. To make the code more robust and prevent runtime errors, it's best to explicitly convert the result to a string. Using str() and providing an empty string as a final fallback will handle None and other falsy values correctly while safely converting any other types.

Suggested change
reasoning_content = delta.get("reasoning_content") or delta.get("reasoning")
reasoning_content = str(delta.get("reasoning_content") or delta.get("reasoning") or "")


output = content
if reasoning_content:
if not is_reasoning:
Expand Down
148 changes: 148 additions & 0 deletions python/tests/interfaces/model/test_llm_ollama_adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import unittest

from dify_plugin.entities.model import AIModelEntity, ModelPropertyKey, ModelType
from dify_plugin.entities.model.llm import LLMMode, LLMResult
from dify_plugin.interfaces.model.large_language_model import LargeLanguageModel


class MockLLM(LargeLanguageModel):
"""
Concrete Mock class for testing non-abstract methods of LargeLanguageModel.
"""

def _invoke(
self,
model: str,
credentials: dict,
prompt_messages: list,
model_parameters: dict,
tools: list,
stop: list,
stream: bool,
user: str,
) -> LLMResult:
pass

def get_num_tokens(self, model: str, credentials: dict, prompt_messages: list, tools: list) -> int:
return 0

def validate_credentials(self, model: str, credentials: dict) -> None:
pass

@property
def _invoke_error_mapping(self) -> dict:
return {}


class TestOllamaAdapter(unittest.TestCase):
def setUp(self):
# Create a dummy model schema to satisfy AIModel.__init__
dummy_schema = AIModelEntity(
model="mock_model",
label={"en_US": "Mock Model"},
model_type=ModelType.LLM,
features=[],
model_properties={ModelPropertyKey.MODE: LLMMode.CHAT.value, ModelPropertyKey.CONTEXT_SIZE: 4096},
parameter_rules=[],
pricing=None,
deprecated=False,
)
self.llm = MockLLM(model_schemas=[dummy_schema])


def test_with_reasoning_content(self):
"""
The test includes reasoning_content,
and the output should contain the <think> tag.
"""

# Simulate simulated streaming data:
# 1. Has reasoning_content

chunks = [
# Chunk 1: Thinking started
{"reasoning_content": "Thinking started.", "content": ""},
# Chunk 2: Still thinking
{"reasoning_content": " Still thinking.", "content": ""},
{"content": "Content started."},
]

# Assume we are testing the logic function itself:
is_reasoning = False
full_output = ""

for chunk in chunks:
# Directly call the implementation in SDK to verify real code logic
output, is_reasoning = self.llm._wrap_thinking_by_reasoning_content(chunk, is_reasoning)
full_output += output

# Verify results
print(f"DEBUG Output: {full_output!r}")

assert "<think>" in full_output
assert "Thinking started. Still thinking." in full_output


def test_with_reasoning(self):
"""
The test includes reasoning,
and the output should contain the <think> tag.
"""

# Simulate simulated streaming data:
# 1. Has reasoning

chunks = [
# Chunk 1: Thinking started
{"reasoning": "Thinking started.", "content": ""},
# Chunk 2: Still thinking
{"reasoning": " Still thinking.", "content": ""},
{"content": "Content started."},
]

# Assume we are testing the logic function itself:
is_reasoning = False
full_output = ""

for chunk in chunks:
# Directly call the implementation in SDK to verify real code logic
output, is_reasoning = self.llm._wrap_thinking_by_reasoning_content(chunk, is_reasoning)
full_output += output

# Verify results
print(f"DEBUG Output: {full_output!r}")

assert "<think>" in full_output
assert "Thinking started. Still thinking." in full_output


def test_without_reasoning(self):
"""
The test does not include reasoning_content or reasoning.
The output should not contain the <think> tag.
"""

# Simulate simulated streaming data:
# 1. Has no reasoning_content and reasoning

chunks = [
# Chunk 1: No Thinking
{"content": "Content started."},
# Chunk 2: Still No thinking
{"content": " Still content."},
]

# Assume we are testing the logic function itself:
is_reasoning = False
full_output = ""

for chunk in chunks:
# Directly call the implementation in SDK to verify real code logic
output, is_reasoning = self.llm._wrap_thinking_by_reasoning_content(chunk, is_reasoning)
full_output += output

# Verify results
print(f"DEBUG Output: {full_output!r}")

assert "<think>" not in full_output
assert "Content started. Still content." in full_output