diff --git a/docs/configuration.md b/docs/configuration.md index d915ceb1c..f3d28bcb3 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -360,7 +360,7 @@ request, while other tools such as `web_fetch` remain available.
DeepSeek native web search -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.
@@ -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. diff --git a/docs/providers.md b/docs/providers.md index 3c3bc2b3d..ff86d23a7 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -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 diff --git a/nanobot/providers/openai_compat_provider.py b/nanobot/providers/openai_compat_provider.py index 37fce20d0..220ae17f1 100644 --- a/nanobot/providers/openai_compat_provider.py +++ b/nanobot/providers/openai_compat_provider.py @@ -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" diff --git a/nanobot/providers/registry.py b/nanobot/providers/registry.py index a5efa41f4..a9608928e 100644 --- a/nanobot/providers/registry.py +++ b/nanobot/providers/registry.py @@ -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 diff --git a/tests/providers/test_litellm_kwargs.py b/tests/providers/test_litellm_kwargs.py index 60b3bd45a..7c8c4de20 100644 --- a/tests/providers/test_litellm_kwargs.py +++ b/tests/providers/test_litellm_kwargs.py @@ -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"), diff --git a/tests/providers/test_responses_circuit_breaker.py b/tests/providers/test_responses_circuit_breaker.py index ca3e6c40a..21beee0ff 100644 --- a/tests/providers/test_responses_circuit_breaker.py +++ b/tests/providers/test_responses_circuit_breaker.py @@ -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):