"""Tests for DeepSeekChatOpenAI thinking-mode behaviour. Two pieces verified: 1. ``reasoning_content`` is captured on receive into the AIMessage's ``additional_kwargs`` and re-attached on send so DeepSeek's API sees the same value across turns. 2. ``with_structured_output`` raises NotImplementedError for ``deepseek-reasoner`` so the agent factories' free-text fallback handles the request instead of failing at runtime. """ import os import pytest from langchain_core.messages import AIMessage, HumanMessage from langchain_core.prompt_values import ChatPromptValue from tradingagents.llm_clients.openai_client import ( DeepSeekChatOpenAI, NormalizedChatOpenAI, _input_to_messages, ) # --------------------------------------------------------------------------- # _input_to_messages — the helper that handles list / ChatPromptValue / other # (Gemini bot review note: non-list inputs must also work) # --------------------------------------------------------------------------- @pytest.mark.unit class TestInputToMessages: def test_list_input_returned_as_is(self): msgs = [HumanMessage(content="hi")] assert _input_to_messages(msgs) is msgs def test_chat_prompt_value_unwrapped(self): msgs = [HumanMessage(content="hi")] prompt_value = ChatPromptValue(messages=msgs) assert _input_to_messages(prompt_value) == msgs def test_string_input_yields_empty_list(self): # A bare string isn't a message-bearing input; the caller's normal # langchain conversion happens upstream of _get_request_payload. assert _input_to_messages("hello") == [] # --------------------------------------------------------------------------- # Reasoning content propagation across turns # --------------------------------------------------------------------------- @pytest.mark.unit class TestDeepSeekReasoningContent: def _client(self): os.environ.setdefault("DEEPSEEK_API_KEY", "placeholder") return DeepSeekChatOpenAI( model="deepseek-v4-flash", api_key="placeholder", base_url="https://api.deepseek.com", ) def test_capture_on_receive(self): """When the response carries reasoning_content, it lands on the AIMessage's additional_kwargs so the next turn can echo it back.""" client = self._client() result = client._create_chat_result( { "model": "deepseek-v4-flash", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "Plan: buy NVDA.", "reasoning_content": "Step 1: trend is up. Step 2: ...", }, "finish_reason": "stop", } ], "usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}, } ) ai = result.generations[0].message assert ai.additional_kwargs["reasoning_content"] == "Step 1: trend is up. Step 2: ..." def test_propagate_on_send(self): """When an outgoing AIMessage carries reasoning_content, the request payload echoes it on the corresponding message dict.""" client = self._client() prior = AIMessage( content="Plan", additional_kwargs={"reasoning_content": "weighed bull case"}, ) new_user = HumanMessage(content="Refine.") payload = client._get_request_payload([prior, new_user]) # Find the assistant message in the payload assistant_dicts = [m for m in payload["messages"] if m.get("role") == "assistant"] assert assistant_dicts, "assistant message missing from outgoing payload" assert assistant_dicts[0]["reasoning_content"] == "weighed bull case" def test_propagate_through_chat_prompt_value(self): """Gemini bot review note: non-list inputs (ChatPromptValue) must also propagate reasoning_content.""" client = self._client() prior = AIMessage( content="Plan", additional_kwargs={"reasoning_content": "weighed bull case"}, ) prompt_value = ChatPromptValue(messages=[prior, HumanMessage(content="Refine.")]) payload = client._get_request_payload(prompt_value) assistant_dicts = [m for m in payload["messages"] if m.get("role") == "assistant"] assert assistant_dicts[0]["reasoning_content"] == "weighed bull case" # --------------------------------------------------------------------------- # deepseek-reasoner: structured output unavailable, falls through to free-text # --------------------------------------------------------------------------- @pytest.mark.unit class TestDeepSeekReasonerStructuredOutput: def test_with_structured_output_raises_for_reasoner(self): client = DeepSeekChatOpenAI( model="deepseek-reasoner", api_key="placeholder", base_url="https://api.deepseek.com", ) from pydantic import BaseModel class _Sample(BaseModel): answer: str with pytest.raises(NotImplementedError): client.with_structured_output(_Sample) def test_with_structured_output_works_for_v4(self): """V4 models (non-reasoner) accept tool_choice; structured output works.""" client = DeepSeekChatOpenAI( model="deepseek-v4-flash", api_key="placeholder", base_url="https://api.deepseek.com", ) from pydantic import BaseModel class _Sample(BaseModel): answer: str # Should return a Runnable, not raise. (The actual API call would # require a real key; we only assert binding succeeds.) wrapped = client.with_structured_output(_Sample) assert wrapped is not None # --------------------------------------------------------------------------- # Base class isolation: NormalizedChatOpenAI does NOT have DeepSeek behaviour # --------------------------------------------------------------------------- @pytest.mark.unit class TestBaseClassIsolation: def test_normalized_does_not_propagate_reasoning_content(self): """The general-purpose NormalizedChatOpenAI must not carry DeepSeek-specific behaviour. Only the subclass does.""" assert not hasattr(NormalizedChatOpenAI, "_get_request_payload") or ( NormalizedChatOpenAI._get_request_payload is NormalizedChatOpenAI.__bases__[0]._get_request_payload )