Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion coding-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Today the repo contains both:
- HTTP server mode via `python -m coding_agent serve`
- Hook-driven runtime built from `agentkit` pipeline stages and plugins
- Tooling for file reads/writes, grep/glob, shell commands, file patching, planning, web search, and subagents
- Provider support for `openai`, `anthropic`, `copilot`, `kimi`, `kimi-code`, and `kimi-code-anthropic`
- Provider support for `openai`, `anthropic`, `copilot`, `kimi`, `kimi-code`, `kimi-code-anthropic`, and `deepseek`
- Approval policies for risky tools and external requests
- Dependency-aware parallel tool execution
- Topic detection, context summarization, memory grounding, session metrics, skills, and MCP server integration
Expand Down Expand Up @@ -121,6 +121,7 @@ Provider-specific fallback keys currently supported by the implementation:
- `GITHUB_TOKEN` for `copilot`
- `MOONSHOT_API_KEY` for `kimi`
- `KIMI_CODE_API_KEY` for `kimi-code` and `kimi-code-anthropic`
- `DEEPSEEK_API_KEY` for `deepseek`

## Repository Layout

Expand Down
2 changes: 2 additions & 0 deletions coding-agent/src/coding_agent/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ def create_child_pipeline(
resolved_key = os.environ.get("MOONSHOT_API_KEY", "")
if not resolved_key and cfg.provider in ("kimi-code", "kimi-code-anthropic"):
resolved_key = os.environ.get("KIMI_CODE_API_KEY", "")
if not resolved_key and cfg.provider == "deepseek":
resolved_key = os.environ.get("DEEPSEEK_API_KEY", "")
resolved_key = resolved_key or ""

approval_cfg = cfg.extra.get("approval", {})
Expand Down
1 change: 1 addition & 0 deletions coding-agent/src/coding_agent/cli/repl.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ def _sync_config_from_managed_session(self, managed_session: Any) -> None:
"kimi",
"kimi-code",
"kimi-code-anthropic",
"deepseek",
}
if provider_name not in allowed_providers:
raise RuntimeError(
Expand Down
6 changes: 6 additions & 0 deletions coding-agent/src/coding_agent/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class Config(BaseModel):
"kimi",
"kimi-code",
"kimi-code-anthropic",
"deepseek",
] = "openai"
model: str = "gpt-4o"
api_key: SecretStr | None = None
Expand Down Expand Up @@ -100,6 +101,11 @@ def load_config(cli_args: dict[str, object] | None = None) -> Config:
if kimi_code_token:
values["api_key"] = kimi_code_token

if values.get("provider") == "deepseek" and not values.get("api_key"):
deepseek_token = os.environ.get("DEEPSEEK_API_KEY")
if deepseek_token:
values["api_key"] = deepseek_token

return Config.model_validate(values)


Expand Down
11 changes: 11 additions & 0 deletions coding-agent/src/coding_agent/plugins/llm_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,17 @@ def provide_llm(self, **kwargs: Any) -> LLMProvider:
base_url="https://api.kimi.com/coding/",
default_headers={"User-Agent": "claude-code/1.0.17"},
)
elif self._provider_name == "deepseek":
import os

from coding_agent.providers.anthropic import AnthropicProvider

api_key = self._api_key or os.environ.get("DEEPSEEK_API_KEY", "")
self._instance = AnthropicProvider(
model=self._model,
api_key=api_key,
base_url="https://api.deepseek.com/anthropic",
)
else:
raise ValueError(f"unsupported provider: {self._provider_name}")

Expand Down
52 changes: 52 additions & 0 deletions coding-agent/tests/coding_agent/plugins/test_llm_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,3 +220,55 @@ def test_kimi_code_anthropic_sets_claude_code_user_agent(self):
assert isinstance(result, AnthropicProvider)
headers = result._client.default_headers
assert headers.get("User-Agent") == "claude-code/1.0.17"


class TestDeepSeekProvider:
def test_deepseek_creates_anthropic_instance(self):
plugin = LLMProviderPlugin(
provider="deepseek",
model="deepseek-v4-pro",
api_key="sk-deepseek-test",
)

result = plugin.provide_llm()

assert isinstance(result, AnthropicProvider)
assert result.model_name == "deepseek-v4-pro"

def test_deepseek_uses_deepseek_anthropic_base_url(self):
plugin = LLMProviderPlugin(
provider="deepseek",
model="deepseek-v4-flash",
api_key="sk-deepseek-test",
)

result = plugin.provide_llm()

assert isinstance(result, AnthropicProvider)
assert result._base_url == "https://api.deepseek.com/anthropic"

def test_deepseek_api_key_env_fallback(self, monkeypatch):
monkeypatch.setenv("DEEPSEEK_API_KEY", "sk-deepseek-from-env")
plugin = LLMProviderPlugin(
provider="deepseek",
model="deepseek-v4-pro",
api_key="",
)

result = plugin.provide_llm()

assert isinstance(result, AnthropicProvider)
assert result._api_key == "sk-deepseek-from-env"

def test_deepseek_explicit_key_takes_priority_over_env(self, monkeypatch):
monkeypatch.setenv("DEEPSEEK_API_KEY", "sk-deepseek-from-env")
plugin = LLMProviderPlugin(
provider="deepseek",
model="deepseek-v4-pro",
api_key="sk-explicit",
)

result = plugin.provide_llm()

assert isinstance(result, AnthropicProvider)
assert result._api_key == "sk-explicit"
45 changes: 45 additions & 0 deletions coding-agent/tests/coding_agent/test_cli_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,30 @@ def patched_init(self_kb, *args, **kwargs):


class TestCliProviderChoices:
def test_top_level_accepts_deepseek_provider(self):
from coding_agent.__main__ import main

with (
patch("coding_agent.__main__.load_config") as mock_load_config,
patch("coding_agent.cli.repl.run_repl", AsyncMock()),
patch("coding_agent.__main__.sys.stdout.isatty", return_value=True),
):
mock_load_config.return_value = MagicMock()
ctx = click.Context(main)
with ctx.scope(cleanup=False):
wrapped_main = getattr(main.callback, "__wrapped__")
wrapped_main(
ctx,
model="deepseek-v4-pro",
provider_name="deepseek",
base_url=None,
api_key=None,
)

mock_load_config.assert_called_once_with(
cli_args={"provider": "deepseek", "model": "deepseek-v4-pro"}
)

def test_top_level_accepts_kimi_code_provider(self):
from coding_agent.__main__ import main

Expand Down Expand Up @@ -491,6 +515,27 @@ def test_run_uses_kimi_code_api_key_env_without_cli_api_key(self, monkeypatch):
assert config.api_key is not None
assert config.api_key.get_secret_value() == "kimi-env-key"

def test_run_uses_deepseek_api_key_env_without_cli_api_key(self, monkeypatch):
from coding_agent.__main__ import main

monkeypatch.delenv("AGENT_API_KEY", raising=False)
monkeypatch.setenv("DEEPSEEK_API_KEY", "deepseek-env-key")
runner = CliRunner()

with patch("coding_agent.__main__._run_headless", AsyncMock()) as mock_run:
result = runner.invoke(
main,
["--provider", "deepseek", "run", "--goal", "test goal"],
)

assert result.exit_code == 0
assert mock_run.await_args is not None
config, goal = mock_run.await_args.args
assert goal == "test goal"
assert config.provider == "deepseek"
assert config.api_key is not None
assert config.api_key.get_secret_value() == "deepseek-env-key"
Comment thread
coderabbitai[bot] marked this conversation as resolved.

def test_run_keeps_run_only_options_local_to_subcommand(self):
from coding_agent.__main__ import main

Expand Down
26 changes: 25 additions & 1 deletion coding-agent/tests/core/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def test_custom_values(self):

@pytest.mark.parametrize(
"provider",
["kimi", "kimi-code", "kimi-code-anthropic"],
["kimi", "kimi-code", "kimi-code-anthropic", "deepseek"],
)
def test_accepts_kimi_family_providers(self, provider):
c = Config(provider=provider)
Expand Down Expand Up @@ -115,3 +115,27 @@ def test_kimi_code_uses_kimi_code_api_key_when_agent_api_key_missing(
assert c.provider == provider
assert c.api_key is not None
assert c.api_key.get_secret_value() == "sk-kimi-code-token"

def test_deepseek_uses_deepseek_api_key_when_agent_api_key_missing(
self, monkeypatch
):
monkeypatch.delenv("AGENT_API_KEY", raising=False)
monkeypatch.setenv("DEEPSEEK_API_KEY", "sk-deepseek-token")

c = load_config(cli_args={"provider": "deepseek"})

assert c.provider == "deepseek"
assert c.api_key is not None
assert c.api_key.get_secret_value() == "sk-deepseek-token"

def test_deepseek_uses_deepseek_api_key_when_agent_api_key_is_empty(
self, monkeypatch
):
monkeypatch.setenv("AGENT_API_KEY", "")
monkeypatch.setenv("DEEPSEEK_API_KEY", "sk-deepseek-token")

c = load_config(cli_args={"provider": "deepseek"})

assert c.provider == "deepseek"
assert c.api_key is not None
assert c.api_key.get_secret_value() == "sk-deepseek-token"