feat(providers): support DeepSeek V4 Pro Responses

This commit is contained in:
chengyongru
2026-08-13 01:02:41 +08:00
committed by chengyongru
parent 01c7323d74
commit edec29e997
6 changed files with 56 additions and 31 deletions
+5 -5
View File
@@ -360,7 +360,7 @@ request, while other tools such as `web_fetch` remain available.
<details>
<summary><b>DeepSeek native web search</b></summary>
DeepSeek V4 Flash uses DeepSeek's native Responses API. Its provider-hosted web search is
DeepSeek V4 Flash and Pro use DeepSeek's native Responses API. Their provider-hosted web search is
enabled by default because it does not require a separate paid add-on. Turn it off from the
WebUI provider settings, or with:
@@ -377,9 +377,9 @@ WebUI provider settings, or with:
}
```
The switch applies to `deepseek-v4-flash`; DeepSeek models that remain on Chat Completions
cannot use this Responses tool. Native search calls appear in the WebUI activity stream, and
their opaque output items are preserved for multi-turn Responses state replay.
The switch applies to `deepseek-v4-flash` and `deepseek-v4-pro`; DeepSeek models that remain on
Chat Completions cannot use this Responses tool. Native search calls appear in the WebUI activity
stream, and their opaque output items are preserved for multi-turn Responses state replay.
</details>
@@ -391,7 +391,7 @@ Providers that use the Responses API can keep reasoning context across a
conversation, which helps with multi-step tasks. Supported providers can also
compact long conversations automatically.
nanobot preserves Responses conversation state automatically for OpenAI Responses, OpenAI Codex, Azure OpenAI, DeepSeek V4 Flash, and compatible GitHub Copilot models.
nanobot preserves Responses conversation state automatically for OpenAI Responses, OpenAI Codex, Azure OpenAI, DeepSeek V4, and compatible GitHub Copilot models.
Native compaction is also automatic when the provider supports it. The
threshold is derived from the active model's context window and reserved output
headroom; no provider configuration is required.
+1 -1
View File
@@ -287,7 +287,7 @@ Arbitrary custom provider names are OpenAI-compatible only; they do not use the
`providers.openai.apiType` may be set when you need to force a specific OpenAI API surface. Other providers reject `apiType`; leave it unset outside `providers.openai`. Replace the model with a model ID available to your OpenAI account. Direct OpenAI Responses, OpenAI Codex, Azure OpenAI Responses, and eligible GitHub Copilot models share [opaque Responses state retention](./configuration.md#responses-state-and-compaction); native compaction is enabled only where the backend supports it. The WebUI exposes provider-native switches for OpenAI web search, Codex Fast mode, DeepSeek web search, and Grok X Search. These switches write the corresponding raw provider request fields under `extraBody`.
DeepSeek is the model-level exception in the OpenAI-compatible provider: `deepseek-v4-flash` automatically uses DeepSeek's native Responses API, while `deepseek-v4-pro` remains on Chat Completions. Its native `web_search` tool is enabled by default and shows its lifecycle in WebUI chat activity; set `providers.deepseek.extraBody.tools` to `[]` to disable it.
DeepSeek is the model-level exception in the OpenAI-compatible provider: `deepseek-v4-flash` and `deepseek-v4-pro` automatically use DeepSeek's native Responses API. Its native `web_search` tool is enabled by default and shows its lifecycle in WebUI chat activity; set `providers.deepseek.extraBody.tools` to `[]` to disable it.
### Custom OpenAI-Compatible Endpoint
+3 -2
View File
@@ -1158,7 +1158,8 @@ class OpenAICompatProvider(LLMProvider):
self._sanitize_empty_content(sanitized_state.pending_messages)
)
)
preserve_reasoning = bool(self._spec and self._spec.name == "deepseek")
is_deepseek = bool(self._spec and self._spec.name == "deepseek")
preserve_reasoning = is_deepseek
instructions, input_items, replayed = prepare_responses_input(
sanitized_messages,
state=sanitized_state,
@@ -1194,7 +1195,7 @@ class OpenAICompatProvider(LLMProvider):
if not self._supports_temperature(model_name, reasoning_effort) and not preserve_reasoning:
body["include"] = ["reasoning.encrypted_content"]
if reasoning_effort and reasoning_effort.lower() != "none":
if reasoning_effort and (reasoning_effort.lower() != "none" or is_deepseek):
body["reasoning"] = {"effort": reasoning_effort}
if replayed and "gpt-5.6" in model_name.lower():
body.setdefault("reasoning", {})["context"] = "all_turns"
+2 -3
View File
@@ -112,8 +112,7 @@ class ProviderSpec:
implicit_reasoning_models: tuple[str, ...] = ()
# Models that expose the OpenAI Responses wire format. This is model-level
# because providers may add Responses support incrementally (DeepSeek V4
# Flash is supported before V4 Pro).
# because providers may add Responses support incrementally.
responses_models: tuple[str, ...] = ()
# Provider-hosted Responses tools sent unless extraBody.tools explicitly
@@ -482,7 +481,7 @@ PROVIDERS: tuple[ProviderSpec, ...] = (
backend="openai_compat",
default_api_base="https://api.deepseek.com",
thinking_style="thinking_type",
responses_models=("deepseek-v4-flash",),
responses_models=("deepseek-v4-flash", "deepseek-v4-pro"),
responses_default_tools=("web_search",),
),
# Gemini: Google's OpenAI-compatible endpoint
+34 -2
View File
@@ -299,8 +299,8 @@ def _fake_chat_stream_legacy_function_call_chunks():
@pytest.mark.asyncio
async def test_openai_compat_stream_forwards_reasoning_deltas_deepseek_style() -> None:
"""Regression: DeepSeek-V4 / reasoner expose ``delta.reasoning_content`` during streaming."""
async def test_openai_compat_chat_stream_forwards_reasoning_deltas_deepseek_style() -> None:
"""DeepSeek Chat Completions exposes ``delta.reasoning_content`` while streaming."""
mock_chat = AsyncMock(return_value=_fake_chat_stream_reasoning_chunks())
spec = find_by_name("deepseek")
thinking: list[str] = []
@@ -321,6 +321,7 @@ async def test_openai_compat_stream_forwards_reasoning_deltas_deepseek_style() -
default_model="deepseek-v4-pro",
spec=spec,
)
provider._api_type = "chat_completions"
result = await provider.chat_stream(
messages=[{"role": "user", "content": "hi"}],
model="deepseek-v4-pro",
@@ -336,6 +337,37 @@ async def test_openai_compat_stream_forwards_reasoning_deltas_deepseek_style() -
mock_chat.assert_awaited_once()
@pytest.mark.asyncio
async def test_deepseek_v4_pro_uses_responses_api() -> None:
mock_chat = AsyncMock(return_value=_fake_chat_response())
mock_responses = AsyncMock(return_value=_fake_responses_response("from responses"))
with patch("nanobot.providers.openai_compat_provider.AsyncOpenAI") as mock_client_class:
client_instance = mock_client_class.return_value
client_instance.chat.completions.create = mock_chat
client_instance.responses.create = mock_responses
provider = OpenAICompatProvider(
api_key="sk-test",
default_model="deepseek-v4-pro",
spec=find_by_name("deepseek"),
)
result = await provider.chat(
messages=[{"role": "user", "content": "hello"}],
model="deepseek-v4-pro",
reasoning_effort="none",
)
assert result.content == "from responses"
mock_responses.assert_awaited_once()
mock_chat.assert_not_awaited()
call_kwargs = mock_responses.call_args.kwargs
assert call_kwargs["model"] == "deepseek-v4-pro"
assert call_kwargs["reasoning"] == {"effort": "none"}
assert call_kwargs["tools"] == [{"type": "web_search"}]
assert "include" not in call_kwargs
@pytest.mark.asyncio
@pytest.mark.parametrize(
("provider_name", "model"),
@@ -11,6 +11,7 @@ from nanobot.providers.openai_compat_provider import (
OpenAICompatProvider,
)
from nanobot.providers.openai_responses.state import build_responses_state
from nanobot.providers.registry import find_by_name
@pytest.fixture()
@@ -30,30 +31,22 @@ def test_responses_api_available_by_default(provider):
assert provider._should_use_responses_api("gpt-5", None) is True
def test_deepseek_v4_flash_uses_responses_by_model(provider):
provider._spec = type("Spec", (), {
"name": "deepseek",
"responses_models": ("deepseek-v4-flash",),
"strip_model_prefix": False,
"strip_model_prefixes": (),
})()
@pytest.mark.parametrize("model", ["deepseek-v4-flash", "deepseek-v4-pro"])
def test_deepseek_v4_models_use_responses_by_model(provider, model):
provider._spec = find_by_name("deepseek")
provider._effective_base = "https://api.deepseek.com"
provider.default_model = "deepseek-v4-flash"
provider.default_model = model
assert provider._should_use_responses_api("deepseek-v4-flash", None) is True
assert provider._should_use_responses_api("deepseek-v4-pro", None) is False
assert provider._should_use_responses_api(model, None) is True
assert provider._should_use_responses_api("deepseek-chat", None) is False
def test_deepseek_v4_flash_matches_provider_prefixed_model(provider):
provider._spec = type("Spec", (), {
"name": "deepseek",
"responses_models": ("deepseek-v4-flash",),
"strip_model_prefix": False,
"strip_model_prefixes": (),
})()
@pytest.mark.parametrize("model", ["deepseek-v4-flash", "deepseek-v4-pro"])
def test_deepseek_v4_models_match_provider_prefixed_model(provider, model):
provider._spec = find_by_name("deepseek")
provider._effective_base = "https://api.deepseek.com"
assert provider._should_use_responses_api("deepseek/deepseek-v4-flash", None) is True
assert provider._should_use_responses_api(f"deepseek/{model}", None) is True
def test_direct_openai_enables_server_compaction(provider):