mirror of
https://github.com/csunny/DB-GPT.git
synced 2026-07-17 10:16:49 +00:00
fix(model): support DeepSeek V4 Pro (#3079)
This commit is contained in:
@@ -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]]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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` |
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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` |
|
||||
|
||||
|
||||
@@ -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}|"
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {}
|
||||
Reference in New Issue
Block a user