fix(model): support DeepSeek V4 Pro (#3079)

This commit is contained in:
Artoriasn
2026-05-27 21:09:36 +08:00
committed by GitHub
parent 5dc365682a
commit 1c9e320521
9 changed files with 220 additions and 27 deletions

View File

@@ -23,9 +23,12 @@ persist_path = "pilot/data"
# Model Configurations
[models]
[[models.llms]]
name = "deepseek-reasoner"
name = "deepseek-v4-pro"
# name = "deepseek-reasoner"
# name = "deepseek-chat"
provider = "proxy/deepseek"
# Disable V4-Pro thinking mode so ReAct responses stay parseable.
thinking_enabled = false
api_key = "your_deepseek_api_key"
[[models.embeddings]]

View File

@@ -45,9 +45,11 @@ Edit `configs/dbgpt-proxy-deepseek.toml`:
```toml
[models]
[[models.llms]]
name = "deepseek-reasoner"
name = "deepseek-v4-pro"
provider = "proxy/deepseek"
api_key = "your-deepseek-api-key"
# Disable V4-Pro thinking mode for strict ReAct output parsing.
thinking_enabled = false
[[models.embeddings]]
name = "BAAI/bge-large-zh-v1.5"
@@ -60,9 +62,14 @@ provider = "hf"
| Model | Config name | Notes |
|---|---|---|
| DeepSeek-V4-Pro | `deepseek-v4-pro` | 1M context, advanced reasoning, coding, and agent tasks |
| DeepSeek-R1 | `deepseek-reasoner` | Strong reasoning, chain-of-thought |
| DeepSeek-V3 | `deepseek-chat` | General purpose chat |
For ReAct agents, keep `thinking_enabled = false` with `deepseek-v4-pro`. DeepSeek
V4-Pro enables thinking mode by default, which can add reasoning blocks before the
strict `Thought/Action/Action Input` response format.
## Start the server
```bash

View File

@@ -44,7 +44,7 @@ export OPENAI_API_KEY="sk-your-actual-key"
| Provider | Example Name |
|---|---|
| OpenAI | `chatgpt_proxyllm`, `gpt-4o` |
| DeepSeek | `deepseek-chat`, `deepseek-reasoner` |
| DeepSeek | `deepseek-v4-pro`, `deepseek-chat`, `deepseek-reasoner` |
| Ollama | `qwen2.5:latest` (must be pulled first) |
| HuggingFace | `THUDM/glm-4-9b-chat-hf` |

View File

@@ -45,9 +45,11 @@ uv sync --all-packages \
```toml
[models]
[[models.llms]]
name = "deepseek-reasoner"
name = "deepseek-v4-pro"
provider = "proxy/deepseek"
api_key = "your-deepseek-api-key"
# 为严格的 ReAct 输出解析关闭 V4-Pro 思考模式。
thinking_enabled = false
[[models.embeddings]]
name = "BAAI/bge-large-zh-v1.5"
@@ -60,9 +62,14 @@ provider = "hf"
| 模型 | 配置名 | 说明 |
|---|---|---|
| DeepSeek-V4-Pro | `deepseek-v4-pro` | 1M 上下文,适合高级推理、代码与 Agent 任务 |
| DeepSeek-R1 | `deepseek-reasoner` | 推理能力强,适合复杂思考任务 |
| DeepSeek-V3 | `deepseek-chat` | 通用聊天与问答 |
ReAct Agent 使用 `deepseek-v4-pro` 时建议保留 `thinking_enabled = false`
DeepSeek V4-Pro 默认开启思考模式,可能在严格的
`Thought/Action/Action Input` 输出格式前产生推理块,导致解析失败。
## 启动服务
```bash

View File

@@ -44,7 +44,7 @@ export OPENAI_API_KEY="sk-your-actual-key"
| Provider | 示例名称 |
|---|---|
| OpenAI | `chatgpt_proxyllm`, `gpt-4o` |
| DeepSeek | `deepseek-chat`, `deepseek-reasoner` |
| DeepSeek | `deepseek-v4-pro`, `deepseek-chat`, `deepseek-reasoner` |
| Ollama | `qwen2.5:latest`(需先拉取) |
| HuggingFace | `THUDM/glm-4-9b-chat-hf` |

View File

@@ -93,9 +93,7 @@ class ReActOutputParser:
def _find_prefix_matches(self, text: str, escaped_prefix: str) -> List[re.Match]:
"""Find line-start ReAct prefix matches outside markdown code fences."""
pattern = re.compile(
self._prefix_line_pattern(escaped_prefix), re.MULTILINE
)
pattern = re.compile(self._prefix_line_pattern(escaped_prefix), re.MULTILINE)
fence_spans = self._markdown_fence_spans(text)
return [
match
@@ -196,9 +194,7 @@ class ReActOutputParser:
text = self._normalize_react_text(text).strip()
# Find all line-start instances of the thought prefix outside code fences.
thought_matches = self._find_prefix_matches(
text, self.thought_prefix_escaped
)
thought_matches = self._find_prefix_matches(text, self.thought_prefix_escaped)
if not thought_matches:
return []
@@ -272,12 +268,8 @@ class ReActOutputParser:
self.action_reason_prefix_escaped
)
action_line = self._prefix_line_pattern(self.action_prefix_escaped)
action_input_line = self._prefix_line_pattern(
self.action_input_prefix_escaped
)
observation_line = self._prefix_line_pattern(
self.observation_prefix_escaped
)
action_input_line = self._prefix_line_pattern(self.action_input_prefix_escaped)
observation_line = self._prefix_line_pattern(self.observation_prefix_escaped)
thought_match = re.search(
rf"{thought_line}(.*?)(?={phase_line}|{action_intention_line}|"

View File

@@ -55,10 +55,7 @@ Action Input: {"sql": "select distinct major_type from assets"}"""
assert len(steps) == 1
assert steps[0].thought == "I need to inspect the database schema."
assert steps[0].action_intention == "Query the available asset categories."
assert (
steps[0].action_reason
== "The user asked about assets by major type."
)
assert steps[0].action_reason == "The user asked about assets by major type."
assert steps[0].action == "query_db"
assert steps[0].action_input == {
"sql": "select distinct major_type from assets"
@@ -486,9 +483,7 @@ Action Input: {"result": "网络设备资产共 30 条"}"""
assert len(all_steps) == 2
assert len(current_steps) == 1
assert current_steps[0].action == "query_db"
assert current_steps[0].action_input == {
"sql": "select count(*) from assets"
}
assert current_steps[0].action_input == {"sql": "select count(*) from assets"}
def test_react_markers_inside_action_input_code_fence_are_not_steps(self):
"""Do not split Action Input code fences that mention ReAct labels."""

View File

@@ -1,8 +1,18 @@
import os
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Dict, Optional, Type, Union, cast
from typing import (
TYPE_CHECKING,
Any,
AsyncIterator,
Dict,
List,
Optional,
Type,
Union,
cast,
)
from dbgpt.core import ModelMetadata
from dbgpt.core import ModelMetadata, ModelOutput
from dbgpt.core.awel.flow import (
TAGS_ORDER_HIGH,
ResourceCategory,
@@ -57,6 +67,16 @@ class DeepSeekDeployModelParameters(OpenAICompatibleDeployModelParameters):
},
)
thinking_enabled: Optional[bool] = field(
default=None,
metadata={
"help": _(
"Whether to enable DeepSeek thinking mode. If None, thinking is "
"disabled for deepseek-v4-pro to keep ReAct output parseable."
),
},
)
async def deepseek_generate_stream(
model: ProxyModel, tokenizer, params, device, context_len=2048
@@ -88,6 +108,7 @@ class DeepseekLLMClient(OpenAILLMClient):
context_length: Optional[int] = None,
openai_client: Optional["ClientType"] = None,
openai_kwargs: Optional[Dict[str, Any]] = None,
thinking_enabled: Optional[bool] = None,
**kwargs,
):
api_base = (
@@ -96,7 +117,9 @@ class DeepseekLLMClient(OpenAILLMClient):
api_key = api_key or os.getenv("DEEPSEEK_API_KEY")
model = model or _DEFAULT_MODEL
if not context_length:
if "deepseek-chat" in model:
if "deepseek-v4" in model:
context_length = 1024 * 1024
elif "deepseek-chat" in model:
context_length = 1024 * 32
elif "deepseek-coder" in model:
context_length = 1024 * 16
@@ -109,6 +132,7 @@ class DeepseekLLMClient(OpenAILLMClient):
"Deepseek API key is required, please set 'DEEPSEEK_API_KEY' in "
"environment variable or pass it to the client."
)
self._thinking_enabled = thinking_enabled
super().__init__(
api_key=api_key,
api_base=api_base,
@@ -124,6 +148,69 @@ class DeepseekLLMClient(OpenAILLMClient):
**kwargs,
)
@classmethod
def new_client(
cls,
model_params: DeepSeekDeployModelParameters,
default_executor=None,
) -> "DeepseekLLMClient":
"""Create a new client with the model parameters."""
return cls(
api_key=model_params.api_key,
api_base=model_params.api_base,
api_type=model_params.api_type,
api_version=model_params.api_version,
model=model_params.real_provider_model_name,
proxy=model_params.http_proxy,
model_alias=model_params.real_provider_model_name,
context_length=model_params.context_length,
thinking_enabled=model_params.thinking_enabled,
)
def _build_request(self, request, stream: Optional[bool] = False) -> Dict[str, Any]:
payload = super()._build_request(request, stream)
model = payload.get("model") or self.default_model
extra_body = payload.get("extra_body") or {}
if "thinking" not in extra_body and self._should_set_thinking(model):
thinking_enabled = self._thinking_enabled is True
extra_body["thinking"] = {
"type": "enabled" if thinking_enabled else "disabled"
}
payload["extra_body"] = extra_body
return payload
def _should_set_thinking(self, model: str) -> bool:
return self._thinking_enabled is not None or model == "deepseek-v4-pro"
def _is_thinking_disabled(self, payload: Dict[str, Any]) -> bool:
thinking = (payload.get("extra_body") or {}).get("thinking") or {}
return thinking.get("type") == "disabled"
def _drop_thinking_if_disabled(
self, output: ModelOutput, payload: Dict[str, Any]
) -> ModelOutput:
if not self._is_thinking_disabled(payload) or not output.has_thinking:
return output
text = output.text if output.has_text else ""
return ModelOutput.build(
text=text,
usage=output.usage,
finish_reason=output.finish_reason,
metrics=output.metrics,
)
async def generate_v1(
self, messages: List[Dict[str, Any]], payload: Dict[str, Any]
) -> ModelOutput:
output = await super().generate_v1(messages, payload)
return self._drop_thinking_if_disabled(output, payload)
async def generate_stream_v1(
self, messages: List[Dict[str, Any]], payload: Dict[str, Any]
) -> AsyncIterator[ModelOutput]:
async for output in super().generate_stream_v1(messages, payload):
yield self._drop_thinking_if_disabled(output, payload)
def check_sdk_version(self, version: str) -> None:
if not version >= "1.0":
raise ValueError(
@@ -154,6 +241,14 @@ class DeepseekLLMClient(OpenAILLMClient):
register_proxy_model_adapter(
DeepseekLLMClient,
supported_models=[
ModelMetadata(
model="deepseek-v4-pro",
context_length=1024 * 1024,
max_output_length=384 * 1024,
description="DeepSeek-V4-Pro by DeepSeek",
link="https://api-docs.deepseek.com/news/news260424",
function_calling=True,
),
ModelMetadata(
model="deepseek-chat",
context_length=64 * 1024,

View File

@@ -0,0 +1,94 @@
"""Tests for DeepSeek proxy LLM client."""
import os
from unittest.mock import patch
from dbgpt.core import ModelMessage, ModelOutput, ModelRequest
from dbgpt.model.proxy.llms.deepseek import DeepseekLLMClient
class TestDeepSeekModelRegistration:
"""DeepSeek provider/model adapter resolution."""
def test_supported_models_contains_v4_pro_metadata(self):
from dbgpt.model.adapter.base import get_model_adapter
adapter = get_model_adapter("proxy/deepseek", "deepseek-v4-pro")
assert adapter is not None
metadata = {metadata.model: metadata for metadata in adapter.supported_models()}
v4_pro = metadata["deepseek-v4-pro"]
assert v4_pro.context_length == 1024 * 1024
assert v4_pro.max_output_length == 384 * 1024
assert v4_pro.function_calling is True
class TestDeepSeekContextLength:
"""DeepSeek model context length defaults."""
@patch.dict(os.environ, {"DEEPSEEK_API_KEY": "test-key"})
def test_v4_pro_uses_one_million_context(self):
client = DeepseekLLMClient(
model="deepseek-v4-pro",
openai_client=_FakeOpenAIClient(),
)
assert client.context_length == 1024 * 1024
assert client._context_length == 1024 * 1024
class TestDeepSeekThinkingMode:
"""DeepSeek V4 thinking mode defaults for parse-sensitive agents."""
def _request(self):
return ModelRequest(
model="deepseek-v4-pro",
messages=[ModelMessage(role="user", content="hi")],
)
@patch.dict(os.environ, {"DEEPSEEK_API_KEY": "test-key"})
def test_v4_pro_disables_thinking_by_default(self):
client = DeepseekLLMClient(
model="deepseek-v4-pro",
openai_client=_FakeOpenAIClient(),
)
payload = client._build_request(self._request())
assert payload["extra_body"]["thinking"] == {"type": "disabled"}
@patch.dict(os.environ, {"DEEPSEEK_API_KEY": "test-key"})
def test_v4_pro_can_enable_thinking_explicitly(self):
client = DeepseekLLMClient(
model="deepseek-v4-pro",
thinking_enabled=True,
openai_client=_FakeOpenAIClient(),
)
payload = client._build_request(self._request())
assert payload["extra_body"]["thinking"] == {"type": "enabled"}
@patch.dict(os.environ, {"DEEPSEEK_API_KEY": "test-key"})
def test_disabled_thinking_drops_reasoning_content(self):
client = DeepseekLLMClient(
model="deepseek-v4-pro",
openai_client=_FakeOpenAIClient(),
)
output = ModelOutput.build(
text="Thought: continue\nAction: terminate\nAction Input: {}",
thinking="private reasoning",
)
payload = client._build_request(self._request())
sanitized = client._drop_thinking_if_disabled(output, payload)
assert sanitized.has_thinking is False
assert (
sanitized.text == "Thought: continue\nAction: terminate\nAction Input: {}"
)
class _FakeOpenAIClient:
default_headers = {}