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
10 changes: 10 additions & 0 deletions camel/configs/deepseek_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,15 @@ class DeepSeekConfig(BaseConfig):
position, each with an associated log probability. logprobs
must be set to true if this parameter is used.
(default: :obj:`None`)
thinking (dict, optional): Controls whether to enable thinking
(reasoning) mode. Set to `{"type": "enabled"}` to enable
thinking mode on `deepseek-chat` model, which makes the model
output reasoning content before the final answer. When using
`deepseek-reasoner` model, thinking mode is enabled by default.
Note: when thinking mode is enabled, `temperature`, `top_p`,
`presence_penalty`, and `frequency_penalty` will not take effect.
Reference: https://api-docs.deepseek.com/guides/thinking_mode
(default: :obj:`None`)
include_usage (bool, optional): When streaming, specifies whether to
include usage information in `stream_options`.
(default: :obj:`None`)
Expand All @@ -98,6 +107,7 @@ class DeepSeekConfig(BaseConfig):
] = None
logprobs: Optional[bool] = None
top_logprobs: Optional[int] = None
thinking: Optional[Dict[str, str]] = None
stream_options: Optional[dict[str, bool]] = None

def __init__(self, include_usage: bool = True, **kwargs):
Expand Down
127 changes: 121 additions & 6 deletions camel/models/deepseek_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
# limitations under the License.
# ========= Copyright 2023-2026 @ CAMEL-AI.org. All Rights Reserved. =========

import copy
import os
from typing import Any, Dict, List, Optional, Type, Union
from typing import Any, Dict, List, Optional, Type, Union, cast

from openai import AsyncStream, Stream
from pydantic import BaseModel
Expand Down Expand Up @@ -56,7 +57,6 @@
"frequency_penalty",
"logprobs",
"top_logprobs",
"tools",
]


Expand Down Expand Up @@ -124,20 +124,103 @@ def __init__(
max_retries=max_retries,
**kwargs,
)
# Map tool_call_id -> reasoning_content for multi-turn tool calls.
# Since ChatAgent memory doesn't store reasoning_content, we need to
# keep all of them and re-inject on every request within the same turn.
# Cleared at the start of a new turn (new user message).
self._reasoning_content_map: Dict[str, str] = {}

def _is_thinking_enabled(self) -> bool:
r"""Check if thinking (reasoning) mode is enabled.

Returns:
bool: Whether thinking mode is enabled.
"""
if self.model_type in [ModelType.DEEPSEEK_REASONER]:
return True
thinking = self.model_config_dict.get("thinking")
return isinstance(thinking, dict) and thinking.get("type") == "enabled"

def _inject_reasoning_content(
self,
messages: List[OpenAIMessage],
) -> List[OpenAIMessage]:
r"""Inject reasoning_content into all assistant messages that need it.

Args:
messages: The original messages list.

Returns:
Messages with reasoning_content injected where needed.
"""
if not messages:
return messages

# New turn (last message is user) -> clear map and return as-is
last_msg = messages[-1]
if isinstance(last_msg, dict) and last_msg.get("role") == "user":
self._reasoning_content_map.clear()
return messages

if not self._reasoning_content_map:
return messages

# Inject reasoning_content into ALL assistant messages that need it
processed: List[OpenAIMessage] = []
for msg in messages:
if (
isinstance(msg, dict)
and msg.get("role") == "assistant"
and msg.get("tool_calls")
and "reasoning_content" not in msg
):
tool_calls = cast(
List[Dict[str, Any]], msg.get("tool_calls", [])
)
if tool_calls:
first_id = tool_calls[0].get("id", "")
reasoning = self._reasoning_content_map.get(first_id)
if reasoning:
new_msg = cast(Dict[str, Any], copy.deepcopy(msg))
new_msg["reasoning_content"] = reasoning
processed.append(cast(OpenAIMessage, new_msg))
continue
processed.append(msg)

return processed

def _store_reasoning_content(self, response: ChatCompletion) -> None:
r"""Store reasoning_content from the model response.

Args:
response: The model response.
"""
if not response.choices:
return

message = response.choices[0].message
reasoning = getattr(message, "reasoning_content", None)
tool_calls = getattr(message, "tool_calls", None)

if reasoning and tool_calls:
for tc in tool_calls:
tc_id = getattr(tc, "id", None)
if tc_id:
self._reasoning_content_map[tc_id] = reasoning

def _prepare_request(
self,
messages: List[OpenAIMessage],
response_format: Optional[Type[BaseModel]] = None,
tools: Optional[List[Dict[str, Any]]] = None,
) -> Dict[str, Any]:
request_config = self.model_config_dict.copy()
request_config = copy.deepcopy(self.model_config_dict)

if self.model_type in [
ModelType.DEEPSEEK_REASONER,
]:
logger.warning(
"Warning: You are using an DeepSeek Reasoner model, "
"Warning: You are using a DeepSeek Reasoner model, "
"which has certain limitations, reference: "
"`https://api-docs.deepseek.com/guides/reasoning_model"
"#api-parameters`.",
Expand All @@ -147,9 +230,14 @@ def _prepare_request(
for key, value in request_config.items()
if key not in REASONSER_UNSUPPORTED_PARAMS
}
import copy

request_config = copy.deepcopy(self.model_config_dict)
thinking = request_config.pop("thinking", None)
if thinking:
request_config["extra_body"] = {
**request_config.get("extra_body", {}),
"thinking": thinking,
}

# Remove strict from each tool's function parameters since DeepSeek
# does not support them
if tools:
Expand All @@ -175,12 +263,19 @@ def _run(
Args:
messages (List[OpenAIMessage]): Message list with the chat history
in OpenAI API format.
response_format (Optional[Type[BaseModel]]): The format of the
response.
tools (Optional[List[Dict[str, Any]]]): The schema of the tools
to use for the request.

Returns:
Union[ChatCompletion, Stream[ChatCompletionChunk]]:
`ChatCompletion` in the non-stream mode, or
`Stream[ChatCompletionChunk]` in the stream mode.
"""
if self._is_thinking_enabled():
messages = self._inject_reasoning_content(messages)

self._log_and_trace()

request_config = self._prepare_request(
Expand All @@ -193,6 +288,12 @@ def _run(
**request_config,
)

# Store reasoning_content for future requests
if self._is_thinking_enabled() and isinstance(
response, ChatCompletion
):
self._store_reasoning_content(response)

return response

@observe()
Expand All @@ -207,21 +308,35 @@ async def _arun(
Args:
messages (List[OpenAIMessage]): Message list with the chat history
in OpenAI API format.
response_format (Optional[Type[BaseModel]]): The format of the
response.
tools (Optional[List[Dict[str, Any]]]): The schema of the tools
to use for the request.

Returns:
Union[ChatCompletion, AsyncStream[ChatCompletionChunk]]:
`ChatCompletion` in the non-stream mode, or
`AsyncStream[ChatCompletionChunk]` in the stream mode.
"""
if self._is_thinking_enabled():
messages = self._inject_reasoning_content(messages)

self._log_and_trace()

request_config = self._prepare_request(
messages, response_format, tools
)

response = await self._async_client.chat.completions.create(
messages=messages,
model=self.model_type,
**request_config,
)

# Store reasoning_content for future requests
if self._is_thinking_enabled() and isinstance(
response, ChatCompletion
):
self._store_reasoning_content(response)

return response
68 changes: 68 additions & 0 deletions test/models/test_deepseek_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
# ========= Copyright 2023-2026 @ CAMEL-AI.org. All Rights Reserved. =========


from unittest.mock import MagicMock

import pytest

from camel.configs import DeepSeekConfig
Expand Down Expand Up @@ -54,3 +56,69 @@ def test_deepseek_model_create(model_type: ModelType):
model_config_dict=DeepSeekConfig(temperature=1.3).as_dict(),
)
assert model.model_type == model_type


@pytest.mark.model_backend
def test_deepseek_config_thinking_parameter():
"""Test DeepSeekConfig accepts thinking parameter."""
config = DeepSeekConfig(thinking={"type": "enabled"})
assert config.as_dict()["thinking"] == {"type": "enabled"}


@pytest.mark.model_backend
def test_is_thinking_enabled():
"""Test _is_thinking_enabled for different configurations."""
model_reasoner = DeepSeekModel(ModelType.DEEPSEEK_REASONER)
assert model_reasoner._is_thinking_enabled() is True

model_chat = DeepSeekModel(ModelType.DEEPSEEK_CHAT)
assert model_chat._is_thinking_enabled() is False

config = DeepSeekConfig(thinking={"type": "enabled"}).as_dict()
model_chat_thinking = DeepSeekModel(ModelType.DEEPSEEK_CHAT, config)
assert model_chat_thinking._is_thinking_enabled() is True


@pytest.mark.model_backend
def test_inject_and_store_reasoning_content():
"""Test reasoning_content injection and storage."""
model = DeepSeekModel(ModelType.DEEPSEEK_REASONER)

mock_tool_call = MagicMock()
mock_tool_call.id = "tc_123"
mock_message = MagicMock()
mock_message.reasoning_content = "My reasoning..."
mock_message.tool_calls = [mock_tool_call]
mock_response = MagicMock()
mock_response.choices = [MagicMock(message=mock_message)]

model._store_reasoning_content(mock_response)
assert model._reasoning_content_map == {"tc_123": "My reasoning..."}

messages = [
{
"role": "assistant",
"content": None,
"tool_calls": [{"id": "tc_123"}],
},
{"role": "tool", "tool_call_id": "tc_123", "content": "result"},
]
result = model._inject_reasoning_content(messages)
assert result[0]["reasoning_content"] == "My reasoning..."

model._inject_reasoning_content([{"role": "user", "content": "new"}])
assert model._reasoning_content_map == {}


@pytest.mark.model_backend
def test_prepare_request_thinking_in_extra_body():
"""Test thinking parameter is moved to extra_body."""
config = DeepSeekConfig(thinking={"type": "enabled"}).as_dict()
model = DeepSeekModel(ModelType.DEEPSEEK_CHAT, model_config_dict=config)

request_config = model._prepare_request(
[{"role": "user", "content": "Hi"}]
)

assert "thinking" not in request_config
assert request_config["extra_body"]["thinking"] == {"type": "enabled"}
Loading