|
1 | 1 | # coding=utf-8 |
2 | 2 | import base64 |
| 3 | +import json |
3 | 4 | from concurrent.futures import ThreadPoolExecutor |
4 | 5 | from requests.exceptions import ConnectTimeout, ReadTimeout |
5 | 6 | from typing import Dict, Optional, Any, Iterator, cast, Union, Sequence, Callable, Mapping |
6 | 7 |
|
7 | 8 | from langchain_core.language_models import LanguageModelInput |
8 | 9 | from langchain_core.messages import BaseMessage, get_buffer_string, BaseMessageChunk, HumanMessageChunk, AIMessageChunk, \ |
9 | 10 | SystemMessageChunk, FunctionMessageChunk, ChatMessageChunk |
10 | | -from langchain_core.messages.ai import UsageMetadata |
| 11 | +from langchain_core.messages.ai import UsageMetadata, AIMessage |
11 | 12 | from langchain_core.messages.tool import tool_call_chunk, ToolMessageChunk |
12 | 13 | from langchain_core.outputs import ChatGenerationChunk |
13 | 14 | from langchain_core.runnables import RunnableConfig, ensure_config |
@@ -218,6 +219,57 @@ def invoke( |
218 | 219 | 'token_usage'] if 'token_usage' in chat_result.response_metadata else chat_result.usage_metadata |
219 | 220 | return chat_result |
220 | 221 |
|
| 222 | + def _get_request_payload( |
| 223 | + self, |
| 224 | + input_: LanguageModelInput, |
| 225 | + *, |
| 226 | + stop: list[str] | None = None, |
| 227 | + **kwargs: Any, |
| 228 | + ) -> dict: |
| 229 | + # Get original messages to preserve reasoning_content before base conversion |
| 230 | + messages = self._convert_input(input_).to_messages() |
| 231 | + # Store reasoning_content for AIMessages with tool_calls |
| 232 | + # According to DeepSeek API docs, reasoning_content is REQUIRED when tool_calls |
| 233 | + # are present during the tool invocation process (within same question/turn). |
| 234 | + # See: https://api-docs.deepseek.com/guides/thinking_mode#tool-calls |
| 235 | + reasoning_content_map = {} |
| 236 | + for i, msg in enumerate(messages): |
| 237 | + if ( |
| 238 | + isinstance(msg, AIMessage) |
| 239 | + and (msg.tool_calls or msg.invalid_tool_calls) |
| 240 | + and (reasoning := msg.additional_kwargs.get("reasoning_content")) |
| 241 | + ): |
| 242 | + reasoning_content_map[i] = reasoning |
| 243 | + |
| 244 | + payload = super()._get_request_payload(input_, stop=stop, **kwargs) |
| 245 | + |
| 246 | + # Restore reasoning_content for assistant messages with tool_calls |
| 247 | + # This is required by DeepSeek API - missing it causes 400 error |
| 248 | + if "messages" in payload and reasoning_content_map: |
| 249 | + for i, message in enumerate(payload["messages"]): |
| 250 | + if ( |
| 251 | + i in reasoning_content_map |
| 252 | + and message.get("role") == "assistant" |
| 253 | + and message.get("tool_calls") |
| 254 | + ): |
| 255 | + message["reasoning_content"] = reasoning_content_map[i] |
| 256 | + |
| 257 | + # Apply DeepSeek-specific message formatting |
| 258 | + for message in payload["messages"]: |
| 259 | + if message["role"] == "tool" and isinstance(message["content"], list): |
| 260 | + message["content"] = json.dumps(message["content"]) |
| 261 | + elif message["role"] == "assistant" and isinstance( |
| 262 | + message["content"], list |
| 263 | + ): |
| 264 | + # DeepSeek API expects assistant content to be a string, not a list. |
| 265 | + # Extract text blocks and join them, or use empty string if none exist. |
| 266 | + text_parts = [ |
| 267 | + block.get("text", "") |
| 268 | + for block in message["content"] |
| 269 | + if isinstance(block, dict) and block.get("type") == "text" |
| 270 | + ] |
| 271 | + message["content"] = "".join(text_parts) if text_parts else "" |
| 272 | + return payload |
221 | 273 |
|
222 | 274 | def upload_file_and_get_url(self, file_stream, file_name): |
223 | 275 | """上传文件并获取文件URL""" |
|
0 commit comments