From 9745421555218a8a07a841884bd6a4537267bfd4 Mon Sep 17 00:00:00 2001 From: liminghao <17756684834@163.com> Date: Mon, 30 Mar 2026 16:34:45 +0800 Subject: [PATCH] add deepseek and kimi provider support --- .env.example | 3 + README.md | 6 +- cli/utils.py | 4 +- main.py | 1 + tests/test_llm_provider_support.py | 68 ++++++++++++++++++++++ tests/test_model_validation.py | 4 +- tradingagents/default_config.py | 2 +- tradingagents/llm_clients/factory.py | 4 +- tradingagents/llm_clients/model_catalog.py | 22 +++++++ tradingagents/llm_clients/openai_client.py | 24 ++++---- tradingagents/llm_clients/validators.py | 4 +- 11 files changed, 122 insertions(+), 20 deletions(-) create mode 100644 tests/test_llm_provider_support.py diff --git a/.env.example b/.env.example index 1328b838..9b2839ca 100644 --- a/.env.example +++ b/.env.example @@ -4,3 +4,6 @@ GOOGLE_API_KEY= ANTHROPIC_API_KEY= XAI_API_KEY= OPENROUTER_API_KEY= +DEEPSEEK_API_KEY= +KIMI_API_KEY= +MOONSHOT_API_KEY= diff --git a/README.md b/README.md index 9a92bff9..fced07bd 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,8 @@ export GOOGLE_API_KEY=... # Google (Gemini) export ANTHROPIC_API_KEY=... # Anthropic (Claude) export XAI_API_KEY=... # xAI (Grok) export OPENROUTER_API_KEY=... # OpenRouter +export DEEPSEEK_API_KEY=... # DeepSeek +export KIMI_API_KEY=... # Kimi (Moonshot) export ALPHA_VANTAGE_API_KEY=... # Alpha Vantage ``` @@ -178,7 +180,7 @@ An interface will appear showing results as they load, letting you track the age ### Implementation Details -We built TradingAgents with LangGraph to ensure flexibility and modularity. The framework supports multiple LLM providers: OpenAI, Google, Anthropic, xAI, OpenRouter, and Ollama. +We built TradingAgents with LangGraph to ensure flexibility and modularity. The framework supports multiple LLM providers: OpenAI, Google, Anthropic, xAI, DeepSeek, Kimi, OpenRouter, and Ollama. ### Python Usage @@ -202,7 +204,7 @@ from tradingagents.graph.trading_graph import TradingAgentsGraph from tradingagents.default_config import DEFAULT_CONFIG config = DEFAULT_CONFIG.copy() -config["llm_provider"] = "openai" # openai, google, anthropic, xai, openrouter, ollama +config["llm_provider"] = "openai" # openai, google, anthropic, xai, deepseek, kimi, openrouter, ollama config["deep_think_llm"] = "gpt-5.4" # Model for complex reasoning config["quick_think_llm"] = "gpt-5.4-mini" # Model for quick tasks config["max_debate_rounds"] = 2 diff --git a/cli/utils.py b/cli/utils.py index e071ce06..b0d0fc8b 100644 --- a/cli/utils.py +++ b/cli/utils.py @@ -240,6 +240,8 @@ def select_llm_provider() -> tuple[str, str | None]: ("Google", None), # google-genai SDK manages its own endpoint ("Anthropic", "https://api.anthropic.com/"), ("xAI", "https://api.x.ai/v1"), + ("DeepSeek", "https://api.deepseek.com/v1"), + ("Kimi", "https://api.moonshot.cn/v1"), ("Openrouter", "https://openrouter.ai/api/v1"), ("Ollama", "http://localhost:11434/v1"), ] @@ -261,7 +263,7 @@ def select_llm_provider() -> tuple[str, str | None]: ).ask() if choice is None: - console.print("\n[red]no OpenAI backend selected. Exiting...[/red]") + console.print("\n[red]No LLM provider selected. Exiting...[/red]") exit(1) display_name, url = choice diff --git a/main.py b/main.py index c94fde32..7bc4fa21 100644 --- a/main.py +++ b/main.py @@ -10,6 +10,7 @@ load_dotenv() config = DEFAULT_CONFIG.copy() config["deep_think_llm"] = "gpt-5.4-mini" # Use a different model config["quick_think_llm"] = "gpt-5.4-mini" # Use a different model +config["llm_provider"] = "openai" # openai, google, anthropic, xai, deepseek, kimi, openrouter, ollama config["max_debate_rounds"] = 1 # Increase debate rounds # Configure data vendors (default uses yfinance, no extra API keys needed) diff --git a/tests/test_llm_provider_support.py b/tests/test_llm_provider_support.py new file mode 100644 index 00000000..9207f258 --- /dev/null +++ b/tests/test_llm_provider_support.py @@ -0,0 +1,68 @@ +import os +import unittest +from unittest.mock import patch + +from tradingagents.llm_clients.factory import create_llm_client +from tradingagents.llm_clients.openai_client import OpenAIClient +from tradingagents.llm_clients.validators import validate_model + + +class LLMProviderSupportTests(unittest.TestCase): + def test_factory_supports_deepseek_and_kimi(self): + deepseek_client = create_llm_client("deepseek", "deepseek-chat") + kimi_client = create_llm_client("kimi", "kimi-latest") + + self.assertIsInstance(deepseek_client, OpenAIClient) + self.assertIsInstance(kimi_client, OpenAIClient) + + @patch("tradingagents.llm_clients.openai_client.NormalizedChatOpenAI") + def test_deepseek_uses_expected_base_url_and_key(self, mock_chat_openai): + with patch.dict(os.environ, {"DEEPSEEK_API_KEY": "deepseek-test-key"}, clear=False): + client = OpenAIClient("deepseek-chat", provider="deepseek") + client.get_llm() + + kwargs = mock_chat_openai.call_args.kwargs + self.assertEqual(kwargs["base_url"], "https://api.deepseek.com/v1") + self.assertEqual(kwargs["api_key"], "deepseek-test-key") + self.assertEqual(kwargs["model"], "deepseek-chat") + + @patch("tradingagents.llm_clients.openai_client.NormalizedChatOpenAI") + def test_kimi_prefers_kimi_api_key(self, mock_chat_openai): + with patch.dict( + os.environ, + { + "KIMI_API_KEY": "kimi-test-key", + "MOONSHOT_API_KEY": "moonshot-test-key", + }, + clear=False, + ): + client = OpenAIClient("kimi-latest", provider="kimi") + client.get_llm() + + kwargs = mock_chat_openai.call_args.kwargs + self.assertEqual(kwargs["base_url"], "https://api.moonshot.cn/v1") + self.assertEqual(kwargs["api_key"], "kimi-test-key") + self.assertEqual(kwargs["model"], "kimi-latest") + + @patch("tradingagents.llm_clients.openai_client.NormalizedChatOpenAI") + def test_kimi_falls_back_to_moonshot_key(self, mock_chat_openai): + with patch.dict( + os.environ, + {"KIMI_API_KEY": "", "MOONSHOT_API_KEY": "moonshot-test-key"}, + clear=False, + ): + client = OpenAIClient("kimi-thinking-preview", provider="kimi") + client.get_llm() + + kwargs = mock_chat_openai.call_args.kwargs + self.assertEqual(kwargs["base_url"], "https://api.moonshot.cn/v1") + self.assertEqual(kwargs["api_key"], "moonshot-test-key") + self.assertEqual(kwargs["model"], "kimi-thinking-preview") + + def test_validator_allows_fast_moving_compatibility_providers(self): + self.assertTrue(validate_model("deepseek", "any-model-name")) + self.assertTrue(validate_model("kimi", "any-model-name")) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_model_validation.py b/tests/test_model_validation.py index 50f26318..3c8f3c42 100644 --- a/tests/test_model_validation.py +++ b/tests/test_model_validation.py @@ -40,8 +40,8 @@ class ModelValidationTests(unittest.TestCase): self.assertIn("not-a-real-openai-model", str(caught[0].message)) self.assertIn("openai", str(caught[0].message)) - def test_openrouter_and_ollama_accept_custom_models_without_warning(self): - for provider in ("openrouter", "ollama"): + def test_compatibility_providers_accept_custom_models_without_warning(self): + for provider in ("openrouter", "ollama", "deepseek", "kimi"): client = DummyLLMClient(provider, "custom-model-name") with self.subTest(provider=provider): diff --git a/tradingagents/default_config.py b/tradingagents/default_config.py index 26a4e4d2..7ade7a5d 100644 --- a/tradingagents/default_config.py +++ b/tradingagents/default_config.py @@ -8,7 +8,7 @@ DEFAULT_CONFIG = { "dataflows/data_cache", ), # LLM settings - "llm_provider": "openai", + "llm_provider": "openai", # openai, google, anthropic, xai, deepseek, kimi, openrouter, ollama "deep_think_llm": "gpt-5.4", "quick_think_llm": "gpt-5.4-mini", "backend_url": "https://api.openai.com/v1", diff --git a/tradingagents/llm_clients/factory.py b/tradingagents/llm_clients/factory.py index 93c2a7d3..023541a9 100644 --- a/tradingagents/llm_clients/factory.py +++ b/tradingagents/llm_clients/factory.py @@ -15,7 +15,7 @@ def create_llm_client( """Create an LLM client for the specified provider. Args: - provider: LLM provider (openai, anthropic, google, xai, ollama, openrouter) + provider: LLM provider (openai, anthropic, google, xai, openrouter, deepseek, kimi, ollama) model: Model name/identifier base_url: Optional base URL for API endpoint **kwargs: Additional provider-specific arguments @@ -34,7 +34,7 @@ def create_llm_client( """ provider_lower = provider.lower() - if provider_lower in ("openai", "ollama", "openrouter"): + if provider_lower in ("openai", "ollama", "openrouter", "deepseek", "kimi"): return OpenAIClient(model, base_url, provider=provider_lower, **kwargs) if provider_lower == "xai": diff --git a/tradingagents/llm_clients/model_catalog.py b/tradingagents/llm_clients/model_catalog.py index fd91c66d..33f9fa7a 100644 --- a/tradingagents/llm_clients/model_catalog.py +++ b/tradingagents/llm_clients/model_catalog.py @@ -63,6 +63,28 @@ MODEL_OPTIONS: ProviderModeOptions = { ("Grok 4.1 Fast (Non-Reasoning) - Speed optimized, 2M ctx", "grok-4-1-fast-non-reasoning"), ], }, + "deepseek": { + "quick": [ + ("DeepSeek Chat - Fast non-thinking mode", "deepseek-chat"), + ("DeepSeek Reasoner - Strong reasoning mode", "deepseek-reasoner"), + ], + "deep": [ + ("DeepSeek Reasoner - Strong reasoning mode", "deepseek-reasoner"), + ("DeepSeek Chat - Fast non-thinking mode", "deepseek-chat"), + ], + }, + "kimi": { + "quick": [ + ("Kimi K2 Turbo (Preview) - High-throughput responses", "kimi-k2-turbo-preview"), + ("Kimi Latest - Rolling latest default", "kimi-latest"), + ("Moonshot V1 32K - Stable long-context", "moonshot-v1-32k"), + ], + "deep": [ + ("Kimi Thinking (Preview) - Dedicated reasoning model", "kimi-thinking-preview"), + ("Kimi K2 0905 (Preview) - Strong coding and agent tasks", "kimi-k2-0905-preview"), + ("Moonshot V1 128K - Stable long-context", "moonshot-v1-128k"), + ], + }, # OpenRouter models are fetched dynamically at CLI runtime. # No static entries needed; any model ID is accepted by the validator. "ollama": { diff --git a/tradingagents/llm_clients/openai_client.py b/tradingagents/llm_clients/openai_client.py index 4f2e1b32..de5047af 100644 --- a/tradingagents/llm_clients/openai_client.py +++ b/tradingagents/llm_clients/openai_client.py @@ -26,19 +26,21 @@ _PASSTHROUGH_KWARGS = ( # Provider base URLs and API key env vars _PROVIDER_CONFIG = { - "xai": ("https://api.x.ai/v1", "XAI_API_KEY"), - "openrouter": ("https://openrouter.ai/api/v1", "OPENROUTER_API_KEY"), - "ollama": ("http://localhost:11434/v1", None), + "xai": ("https://api.x.ai/v1", ("XAI_API_KEY",)), + "openrouter": ("https://openrouter.ai/api/v1", ("OPENROUTER_API_KEY",)), + "deepseek": ("https://api.deepseek.com/v1", ("DEEPSEEK_API_KEY",)), + "kimi": ("https://api.moonshot.cn/v1", ("KIMI_API_KEY", "MOONSHOT_API_KEY")), + "ollama": ("http://localhost:11434/v1", ()), } class OpenAIClient(BaseLLMClient): - """Client for OpenAI, Ollama, OpenRouter, and xAI providers. + """Client for OpenAI-compatible providers. For native OpenAI models, uses the Responses API (/v1/responses) which supports reasoning_effort with function tools across all model families (GPT-4.1, GPT-5). Third-party compatible providers (xAI, OpenRouter, - Ollama) use standard Chat Completions. + DeepSeek, Kimi, Ollama) use standard Chat Completions. """ def __init__( @@ -58,12 +60,14 @@ class OpenAIClient(BaseLLMClient): # Provider-specific base URL and auth if self.provider in _PROVIDER_CONFIG: - base_url, api_key_env = _PROVIDER_CONFIG[self.provider] + base_url, api_key_envs = _PROVIDER_CONFIG[self.provider] llm_kwargs["base_url"] = base_url - if api_key_env: - api_key = os.environ.get(api_key_env) - if api_key: - llm_kwargs["api_key"] = api_key + if api_key_envs: + for api_key_env in api_key_envs: + api_key = os.environ.get(api_key_env) + if api_key: + llm_kwargs["api_key"] = api_key + break else: llm_kwargs["api_key"] = "ollama" elif self.base_url: diff --git a/tradingagents/llm_clients/validators.py b/tradingagents/llm_clients/validators.py index 4e6d457b..ea8e5408 100644 --- a/tradingagents/llm_clients/validators.py +++ b/tradingagents/llm_clients/validators.py @@ -13,11 +13,11 @@ VALID_MODELS = { def validate_model(provider: str, model: str) -> bool: """Check if model name is valid for the given provider. - For ollama, openrouter - any model is accepted. + For compatibility providers with fast-moving model catalogs, any model is accepted. """ provider_lower = provider.lower() - if provider_lower in ("ollama", "openrouter"): + if provider_lower in ("ollama", "openrouter", "deepseek", "kimi"): return True if provider_lower not in VALID_MODELS: