feat: add Moonshot and Kimi Coding Plan as LLM providers (#128)

* feat: add Moonshot and Kimi Coding Plan as LLM providers

Add two new providers for Moonshot AI:
- `moonshot`: OpenAI-compatible direct API (api.moonshot.cn/v1) with
  kimi-k2.5, kimi-k2-thinking, moonshot-v1-auto/128k/32k/8k models
- `kimi-coding`: Anthropic-compatible Kimi Coding Plan endpoint
  (api.kimi.com/coding/) with User-Agent header for compatibility

Both providers disable thinking to avoid multi-turn tool calling
errors caused by LangChain dropping reasoning_content from history.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update Moonshot thinking comment and add provider assertions

- Add clarifying comment for disabling thinking on all Moonshot models
- Add moonshot and kimi-coding assertions to test_entries_has_all_providers

* fix: exclude Moonshot and Kimi Coding from content patch

Tested and verified both APIs support standard list content format:
- Moonshot (OpenAI-compatible): supports list content, no patch needed
- Kimi Coding (Anthropic-compatible): supports list content, no patch needed

Only apply _patch_openai_compat_content to strict providers like DeepSeek.

* fix: set _original_provider in routed provider branches

Ensure _original_provider is set before provider is reassigned to
'openai' or 'anthropic', so the no-patch exclusion for Moonshot
and Kimi Coding works correctly.

* style: translate Moonshot comments to English

* style: translate comment to English to fix ruff lint error

* merge: resolve conflicts

* chore: revert uv.lock and translate Chinese comments to English

Revert unrelated uv.lock dependency changes and replace Chinese code
comments with English for codebase consistency per review feedback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: fix ruff format for models.py

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: ypd <ypd@ypddeMac-mini.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Xi Zhang <106144707+X-iZhang@users.noreply.github.com>
Co-authored-by: Xiaohui Yan <xhcloud@gmail.com>
Co-authored-by: X-iZhang <zacharyzhang2022@gmail.com>
This commit is contained in:
Peidong Yang
2026-04-09 02:53:49 +08:00
committed by GitHub
parent 632159261c
commit 066f36fa13
5 changed files with 121 additions and 3 deletions
+2
View File
@@ -11,6 +11,8 @@ MINIMAX_API_KEY= # platform.minimaxi.com (Anthropic-compatible)
ZHIPU_API_KEY= # open.bigmodel.cn (智谱)
VOLCENGINE_API_KEY= # volcengine.com (火山引擎)
DASHSCOPE_API_KEY= # dashscope.aliyuncs.com (阿里云)
MOONSHOT_API_KEY= # platform.moonshot.cn (月之暗面)
KIMI_API_KEY= # kimi.com/code (Kimi 代码计划)
# Aggregator platforms (optional)
SILICONFLOW_API_KEY= # siliconflow.cn
+77
View File
@@ -479,6 +479,63 @@ def validate_dashscope_key(api_key: str) -> tuple[bool, str]:
return False, f"Error: {e}"
def validate_moonshot_key(api_key: str) -> tuple[bool, str]:
"""Validate a Moonshot API key by making a test request.
Returns:
Tuple of (is_valid, message).
"""
if not api_key:
return True, "Skipped (no key provided)"
try:
import openai
client = openai.OpenAI(
api_key=api_key,
base_url="https://api.moonshot.cn/v1",
)
client.models.list()
return True, "Valid"
except Exception as e:
error_str = str(e).lower()
if any(
k in error_str for k in ("401", "unauthorized", "invalid", "authentication")
):
return False, "Invalid API key"
return False, f"Error: {e}"
def validate_kimi_key(api_key: str) -> tuple[bool, str]:
"""Validate a Kimi Coding Plan API key by making a test request.
Uses the Anthropic-compatible endpoint at api.kimi.com/coding/.
Returns:
Tuple of (is_valid, message).
"""
if not api_key:
return True, "Skipped (no key provided)"
try:
import anthropic
client = anthropic.Anthropic(
api_key=api_key,
base_url="https://api.kimi.com/coding/",
default_headers={"User-Agent": "claude-code/0.1.0"},
)
client.models.list()
return True, "Valid"
except Exception as e:
error_str = str(e).lower()
if any(
k in error_str for k in ("401", "unauthorized", "invalid", "authentication")
):
return False, "Invalid API key"
return False, f"Error: {e}"
def validate_tavily_key(api_key: str) -> tuple[bool, str]:
"""Validate a Tavily API key by making a test request.
@@ -652,6 +709,14 @@ def _step_provider(config: EvoScientistConfig) -> str:
title="DeepSeek (DeepSeek-R1, DeepSeek-V3)",
value="deepseek",
),
Choice(
title="Moonshot (月之暗面 — Moonshot models)",
value="moonshot",
),
Choice(
title="Kimi Coding Plan (Kimi 代码计划 — coding-focused)",
value="kimi-coding",
),
# Local
Choice(title="Ollama (local models)", value="ollama"),
# Third-party / aggregator
@@ -751,6 +816,16 @@ def _provider_key_info(config: EvoScientistConfig, provider: str):
config.dashscope_api_key or os.environ.get("DASHSCOPE_API_KEY", ""),
validate_dashscope_key,
),
"moonshot": (
"Moonshot",
config.moonshot_api_key or os.environ.get("MOONSHOT_API_KEY", ""),
validate_moonshot_key,
),
"kimi-coding": (
"Kimi Coding Plan",
config.kimi_api_key or os.environ.get("KIMI_API_KEY", ""),
validate_kimi_key,
),
"custom-openai": (
"OpenAI-compatible",
config.custom_openai_api_key or os.environ.get("CUSTOM_OPENAI_API_KEY", ""),
@@ -2789,6 +2864,8 @@ def run_onboard(skip_validation: bool = False) -> bool:
"zhipu-code": "zhipu_api_key",
"volcengine": "volcengine_api_key",
"dashscope": "dashscope_api_key",
"moonshot": "moonshot_api_key",
"kimi-coding": "kimi_api_key",
"custom-openai": "custom_openai_api_key",
"custom-anthropic": "custom_anthropic_api_key",
}
+8
View File
@@ -73,6 +73,8 @@ class EvoScientistConfig:
zhipu_api_key: str = ""
volcengine_api_key: str = ""
dashscope_api_key: str = ""
moonshot_api_key: str = ""
kimi_api_key: str = ""
custom_openai_api_key: str = ""
custom_openai_base_url: str = ""
custom_anthropic_api_key: str = ""
@@ -369,6 +371,8 @@ _ENV_MAPPINGS = {
"zhipu_api_key": "ZHIPU_API_KEY",
"volcengine_api_key": "VOLCENGINE_API_KEY",
"dashscope_api_key": "DASHSCOPE_API_KEY",
"moonshot_api_key": "MOONSHOT_API_KEY",
"kimi_api_key": "KIMI_API_KEY",
"custom_openai_api_key": "CUSTOM_OPENAI_API_KEY",
"custom_openai_base_url": "CUSTOM_OPENAI_BASE_URL",
"custom_anthropic_api_key": "CUSTOM_ANTHROPIC_API_KEY",
@@ -460,6 +464,10 @@ def apply_config_to_env(config: EvoScientistConfig) -> None:
os.environ["VOLCENGINE_API_KEY"] = config.volcengine_api_key
if config.dashscope_api_key and not os.environ.get("DASHSCOPE_API_KEY"):
os.environ["DASHSCOPE_API_KEY"] = config.dashscope_api_key
if config.moonshot_api_key and not os.environ.get("MOONSHOT_API_KEY"):
os.environ["MOONSHOT_API_KEY"] = config.moonshot_api_key
if config.kimi_api_key and not os.environ.get("KIMI_API_KEY"):
os.environ["KIMI_API_KEY"] = config.kimi_api_key
if config.custom_openai_api_key and not os.environ.get("CUSTOM_OPENAI_API_KEY"):
os.environ["CUSTOM_OPENAI_API_KEY"] = config.custom_openai_api_key
if config.custom_openai_base_url and not os.environ.get("CUSTOM_OPENAI_BASE_URL"):
+30 -3
View File
@@ -29,11 +29,14 @@ _VOLCENGINE_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3"
_DASHSCOPE_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
_DEEPSEEK_BASE_URL = "https://api.deepseek.com"
_MOONSHOT_BASE_URL = "https://api.moonshot.cn/v1"
_KIMI_CODING_BASE_URL = "https://api.kimi.com/coding/"
# Providers routed through the OpenAI provider with a custom base_url.
# Maps provider name → (base_url or None, env var for API key).
_OPENAI_ROUTED_PROVIDERS: dict[str, tuple[str | None, str]] = {
"deepseek": (_DEEPSEEK_BASE_URL, "DEEPSEEK_API_KEY"),
"moonshot": (_MOONSHOT_BASE_URL, "MOONSHOT_API_KEY"),
"siliconflow": (_SILICONFLOW_BASE_URL, "SILICONFLOW_API_KEY"),
"zhipu": (_ZHIPU_BASE_URL, "ZHIPU_API_KEY"),
"zhipu-code": (_ZHIPU_CODE_BASE_URL, "ZHIPU_API_KEY"),
@@ -49,6 +52,7 @@ _OPENAI_ROUTED_PROVIDERS: dict[str, tuple[str | None, str]] = {
# Maps provider name → (base_url or None, env var for API key).
_ANTHROPIC_ROUTED_PROVIDERS: dict[str, tuple[str | None, str]] = {
"minimax": (_MINIMAX_ANTHROPIC_BASE_URL, "MINIMAX_API_KEY"),
"kimi-coding": (_KIMI_CODING_BASE_URL, "KIMI_API_KEY"),
"custom-anthropic": (None, "CUSTOM_ANTHROPIC_API_KEY"),
}
@@ -160,6 +164,16 @@ _MODEL_ENTRIES: list[tuple[str, str, str]] = [
# DeepSeek
("deepseek-r1", "deepseek-reasoner", "deepseek"),
("deepseek-v3", "deepseek-chat", "deepseek"),
# Moonshot (OpenAI-compatible)
("kimi-k2.5", "kimi-k2.5", "moonshot"),
("kimi-k2-thinking", "kimi-k2-thinking", "moonshot"),
("kimi-k2-thinking-turbo", "kimi-k2-thinking-turbo", "moonshot"),
("moonshot-v1-auto", "moonshot-v1-auto", "moonshot"),
("moonshot-v1-128k", "moonshot-v1-128k", "moonshot"),
("moonshot-v1-32k", "moonshot-v1-32k", "moonshot"),
("moonshot-v1-8k", "moonshot-v1-8k", "moonshot"),
# Kimi Coding Plan (Anthropic-compatible)
("kimi-for-coding", "kimi-for-coding", "kimi-coding"),
]
# Public dict for simple lookups (last entry wins for duplicate names).
@@ -321,6 +335,7 @@ def get_chat_model(
# OpenAI-routed providers → route through OpenAI provider with base_url
elif provider in _OPENAI_ROUTED_PROVIDERS:
_original_provider = provider
base_url_default, api_key_env = _OPENAI_ROUTED_PROVIDERS[provider]
if provider == "custom-openai":
base_url = os.environ.get("CUSTOM_OPENAI_BASE_URL", "")
@@ -342,6 +357,11 @@ def get_chat_model(
# from history, causing error 20015 on multi-turn requests.
if provider == "siliconflow":
kwargs.setdefault("extra_body", {})["enable_thinking"] = False
# Moonshot: disable thinking for all models to prevent LangChain from dropping
# reasoning_content, which causes multi-turn conversation errors (error 20015).
# Even native thinking models like kimi-k2-thinking operate in non-thinking mode.
if provider == "moonshot":
kwargs.setdefault("extra_body", {})["thinking"] = {"type": "disabled"}
provider = "openai"
# OpenRouter → native ChatOpenRouter via init_chat_model.
@@ -357,6 +377,7 @@ def get_chat_model(
# Anthropic-routed providers → route through Anthropic provider with base_url
elif provider in _ANTHROPIC_ROUTED_PROVIDERS:
_original_provider = provider
base_url_default, api_key_env = _ANTHROPIC_ROUTED_PROVIDERS[provider]
if provider == "custom-anthropic":
base_url = os.environ.get("CUSTOM_ANTHROPIC_BASE_URL", "")
@@ -374,7 +395,9 @@ def get_chat_model(
api_key = os.environ.get(api_key_env, "")
if api_key:
kwargs["api_key"] = api_key
_original_provider = provider
# Kimi Coding Plan requires claude-code User-Agent header
if provider == "kimi-coding":
kwargs.setdefault("default_headers", {})["User-Agent"] = "claude-code/0.1.0"
provider = "anthropic"
elif provider == "ollama":
@@ -399,10 +422,14 @@ def get_chat_model(
chat_model = init_chat_model(model=model_id, model_provider=provider, **kwargs)
# Flatten list content to strings for OpenAI-compatible providers
# Flatten list content to strings for strict OpenAI-compatible providers
# (DeepSeek, SiliconFlow, OpenRouter, custom-openai, etc.) and
# native OpenAI through a proxy, to avoid "sequence expected string" errors.
if _is_third_party or _is_openai_proxy:
# Moonshot and Kimi Coding support standard format, no patch needed.
_no_patch_providers = {"moonshot", "kimi-coding"}
if (
_is_third_party or _is_openai_proxy
) and _original_provider not in _no_patch_providers:
_patch_openai_compat_content(chat_model)
return chat_model
+4
View File
@@ -39,6 +39,8 @@ class TestModelsRegistry:
assert "volcengine" in providers
assert "dashscope" in providers
assert "deepseek" in providers
assert "moonshot" in providers
assert "kimi-coding" in providers
def test_entries_are_valid_tuples(self):
"""Test that _MODEL_ENTRIES contains valid (name, model_id, provider) tuples."""
@@ -57,6 +59,8 @@ class TestModelsRegistry:
"custom-openai",
"custom-anthropic",
"deepseek",
"moonshot",
"kimi-coding",
}
for entry in _MODEL_ENTRIES:
assert len(entry) == 3, f"Entry {entry} doesn't have 3 elements"