free-claude-code/tests/providers/test_deepseek.py
Pavel Yurchenko e719e4aed2
feat: deepseek api support (#118)
## Summary

* add native DeepSeek provider support via the shared OpenAI-compatible
provider base
* allow `deepseek/...` model prefixes in config validation
* add `DEEPSEEK_API_KEY` and `DEEPSEEK_BASE_URL` settings
* add DeepSeek entries to `.env.example` and `config/env.example`
* implement `DeepSeekProvider` and register it in provider dependencies
* add a DeepSeek request builder with DeepSeek-specific thinking payload
handling
* preserve Anthropic thinking blocks as `reasoning_content` for
DeepSeek-compatible continuation flows
* update `claude-pick` to discover DeepSeek models from the DeepSeek API
* document DeepSeek usage in `README.md`
* add tests for config validation, provider dependency wiring, request
building, and streaming behavior

## Motivation

DeepSeek exposes an OpenAI-compatible API and can be used directly
without routing through OpenRouter. This lets users spend their existing
DeepSeek balance through the proxy while keeping the same Claude Code
workflow and per-model provider mapping.

## Example

```dotenv
DEEPSEEK_API_KEY="sk-..."
DEEPSEEK_BASE_URL="https://api.deepseek.com"

MODEL_OPUS="deepseek/deepseek-reasoner"
MODEL_SONNET="deepseek/deepseek-chat"
MODEL_HAIKU="deepseek/deepseek-chat"
MODEL="deepseek/deepseek-chat"

---------

Co-authored-by: Alishahryar1 <alishahryar2@gmail.com>
2026-04-22 17:06:01 -07:00

181 lines
5.6 KiB
Python

"""Tests for DeepSeek provider."""
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from providers.base import ProviderConfig
from providers.deepseek import DEEPSEEK_BASE_URL, DeepSeekProvider
class MockMessage:
def __init__(self, role, content):
self.role = role
self.content = content
class MockBlock:
def __init__(self, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
class MockRequest:
def __init__(self, **kwargs):
self.model = "deepseek-chat"
self.messages = [MockMessage("user", "Hello")]
self.max_tokens = 100
self.temperature = 0.5
self.top_p = 0.9
self.system = "System prompt"
self.stop_sequences = None
self.tools = []
self.extra_body = {}
self.thinking = MagicMock()
self.thinking.enabled = True
for key, value in kwargs.items():
setattr(self, key, value)
@pytest.fixture
def deepseek_config():
return ProviderConfig(
api_key="test_deepseek_key",
base_url=DEEPSEEK_BASE_URL,
rate_limit=10,
rate_window=60,
enable_thinking=True,
)
@pytest.fixture(autouse=True)
def mock_rate_limiter():
"""Mock the global rate limiter to prevent waiting."""
with patch("providers.openai_compat.GlobalRateLimiter") as mock:
instance = mock.get_instance.return_value
instance.wait_if_blocked = AsyncMock(return_value=False)
async def _passthrough(fn, *args, **kwargs):
return await fn(*args, **kwargs)
instance.execute_with_retry = AsyncMock(side_effect=_passthrough)
yield instance
@pytest.fixture
def deepseek_provider(deepseek_config):
return DeepSeekProvider(deepseek_config)
def test_init(deepseek_config):
"""Test provider initialization."""
with patch("providers.openai_compat.AsyncOpenAI") as mock_openai:
provider = DeepSeekProvider(deepseek_config)
assert provider._api_key == "test_deepseek_key"
assert provider._base_url == DEEPSEEK_BASE_URL
mock_openai.assert_called_once()
def test_build_request_body_enables_thinking_for_chat_model(deepseek_provider):
"""Thinking-enabled requests add DeepSeek's thinking payload for chat model."""
req = MockRequest(model="deepseek-chat")
body = deepseek_provider._build_request_body(req)
assert body["model"] == "deepseek-chat"
assert body["extra_body"]["thinking"] == {"type": "enabled"}
assert body["messages"][0]["role"] == "system"
def test_build_request_body_global_disable_blocks_request_thinking():
"""Global disable suppresses provider-side thinking even if the request enables it."""
provider = DeepSeekProvider(
ProviderConfig(
api_key="test_deepseek_key",
base_url=DEEPSEEK_BASE_URL,
rate_limit=10,
rate_window=60,
enable_thinking=False,
)
)
req = MockRequest(model="deepseek-chat")
body = provider._build_request_body(req)
assert "extra_body" not in body or "thinking" not in body["extra_body"]
def test_build_request_body_request_disable_blocks_global_thinking(deepseek_provider):
"""Request-level disable suppresses provider-side thinking when global is enabled."""
req = MockRequest(model="deepseek-chat")
req.thinking.enabled = False
body = deepseek_provider._build_request_body(req)
assert "extra_body" not in body or "thinking" not in body["extra_body"]
def test_build_request_body_reasoner_skips_thinking_extra(deepseek_provider):
"""deepseek-reasoner does not need an extra thinking payload."""
req = MockRequest(model="deepseek-reasoner")
body = deepseek_provider._build_request_body(req)
assert body["model"] == "deepseek-reasoner"
assert "extra_body" not in body or "thinking" not in body["extra_body"]
def test_build_request_body_preserves_caller_thinking_override(deepseek_provider):
"""Caller-provided thinking payload should not be overwritten."""
req = MockRequest(
model="deepseek-chat",
extra_body={"thinking": {"type": "manual"}},
)
body = deepseek_provider._build_request_body(req)
assert body["extra_body"]["thinking"] == {"type": "manual"}
def test_build_request_body_preserves_reasoning_content(deepseek_provider):
"""Thinking blocks are mirrored into reasoning_content for continuation."""
req = MockRequest(
system=None,
messages=[
MockMessage(
"assistant",
[
MockBlock(type="thinking", thinking="First think"),
MockBlock(type="text", text="Then answer"),
],
)
],
)
body = deepseek_provider._build_request_body(req)
assert body["messages"][0]["reasoning_content"] == "First think"
@pytest.mark.asyncio
async def test_stream_response_reasoning_content(deepseek_provider):
"""reasoning_content deltas are emitted as thinking blocks."""
req = MockRequest()
mock_chunk = MagicMock()
mock_chunk.choices = [
MagicMock(
delta=MagicMock(content=None, reasoning_content="Thinking..."),
finish_reason="stop",
)
]
mock_chunk.usage = MagicMock(completion_tokens=2)
async def mock_stream():
yield mock_chunk
with patch.object(
deepseek_provider._client.chat.completions, "create", new_callable=AsyncMock
) as mock_create:
mock_create.return_value = mock_stream()
events = [event async for event in deepseek_provider.stream_response(req)]
assert any(
'"thinking_delta"' in event and "Thinking..." in event for event in events
)