From b00800d633ed892a375aca6d55e208860fef967a Mon Sep 17 00:00:00 2001 From: danielsu Date: Wed, 18 Mar 2026 16:54:01 +0800 Subject: [PATCH 1/6] Add Kimi Code provider --- astrbot/core/config/default.py | 14 +++++ astrbot/core/provider/manager.py | 4 ++ .../core/provider/sources/anthropic_source.py | 20 +++++- .../core/provider/sources/kimi_code_source.py | 31 +++++++++ tests/test_anthropic_kimi_code_provider.py | 63 +++++++++++++++++++ 5 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 astrbot/core/provider/sources/kimi_code_source.py create mode 100644 tests/test_anthropic_kimi_code_provider.py diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 5b4ea7686a..ce82fe4cdc 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -1112,6 +1112,20 @@ class ChatProviderTemplate(TypedDict): "api_base": "https://api.anthropic.com/v1", "timeout": 120, "proxy": "", + "custom_headers": {}, + "anth_thinking_config": {"type": "", "budget": 0, "effort": ""}, + }, + "Kimi Code": { + "id": "kimi-code", + "provider": "kimi-code", + "type": "kimi_code_chat_completion", + "provider_type": "chat_completion", + "enable": True, + "key": [], + "api_base": "https://api.kimi.com/coding/", + "timeout": 120, + "proxy": "", + "custom_headers": {"User-Agent": "claude-code/0.1.0"}, "anth_thinking_config": {"type": "", "budget": 0, "effort": ""}, }, "Moonshot": { diff --git a/astrbot/core/provider/manager.py b/astrbot/core/provider/manager.py index 0df9f791ae..761b84c9db 100644 --- a/astrbot/core/provider/manager.py +++ b/astrbot/core/provider/manager.py @@ -375,6 +375,10 @@ def dynamic_import_provider(self, type: str) -> None: from .sources.anthropic_source import ( ProviderAnthropic as ProviderAnthropic, ) + case "kimi_code_chat_completion": + from .sources.kimi_code_source import ( + ProviderKimiCode as ProviderKimiCode, + ) case "googlegenai_chat_completion": from .sources.gemini_source import ( ProviderGoogleGenAI as ProviderGoogleGenAI, diff --git a/astrbot/core/provider/sources/anthropic_source.py b/astrbot/core/provider/sources/anthropic_source.py index be70fdc74d..48fdea81fc 100644 --- a/astrbot/core/provider/sources/anthropic_source.py +++ b/astrbot/core/provider/sources/anthropic_source.py @@ -29,6 +29,16 @@ "Anthropic Claude API 提供商适配器", ) class ProviderAnthropic(Provider): + @staticmethod + def _normalize_custom_headers(provider_config: dict) -> dict[str, str] | None: + custom_headers = provider_config.get("custom_headers", {}) + if not isinstance(custom_headers, dict) or not custom_headers: + return None + normalized_headers: dict[str, str] = {} + for key, value in custom_headers.items(): + normalized_headers[str(key)] = str(value) + return normalized_headers or None + def __init__( self, provider_config, @@ -46,6 +56,7 @@ def __init__( if isinstance(self.timeout, str): self.timeout = int(self.timeout) self.thinking_config = provider_config.get("anth_thinking_config", {}) + self.custom_headers = self._normalize_custom_headers(provider_config) if use_api_key: self._init_api_key(provider_config) @@ -66,7 +77,14 @@ def _init_api_key(self, provider_config: dict) -> None: def _create_http_client(self, provider_config: dict) -> httpx.AsyncClient | None: """创建带代理的 HTTP 客户端""" proxy = provider_config.get("proxy", "") - return create_proxy_client("Anthropic", proxy) + client = create_proxy_client("Anthropic", proxy) + if client is not None: + if self.custom_headers: + client.headers.update(self.custom_headers) + return client + if self.custom_headers: + return httpx.AsyncClient(headers=self.custom_headers) + return None def _apply_thinking_config(self, payloads: dict) -> None: thinking_type = self.thinking_config.get("type", "") diff --git a/astrbot/core/provider/sources/kimi_code_source.py b/astrbot/core/provider/sources/kimi_code_source.py new file mode 100644 index 0000000000..2f76676678 --- /dev/null +++ b/astrbot/core/provider/sources/kimi_code_source.py @@ -0,0 +1,31 @@ +from ..register import register_provider_adapter +from .anthropic_source import ProviderAnthropic + +KIMI_CODE_API_BASE = "https://api.kimi.com/coding/" +KIMI_CODE_DEFAULT_MODEL = "kimi-code" +KIMI_CODE_USER_AGENT = "claude-code/0.1.0" + + +@register_provider_adapter( + "kimi_code_chat_completion", + "Kimi Code Chat Completion 提供商适配器", +) +class ProviderKimiCode(ProviderAnthropic): + def __init__( + self, + provider_config: dict, + provider_settings: dict, + ) -> None: + merged_provider_config = dict(provider_config) + merged_provider_config.setdefault("api_base", KIMI_CODE_API_BASE) + merged_provider_config.setdefault("model", KIMI_CODE_DEFAULT_MODEL) + + custom_headers = merged_provider_config.get("custom_headers", {}) + if not isinstance(custom_headers, dict): + custom_headers = {} + merged_headers = {str(key): str(value) for key, value in custom_headers.items()} + if not merged_headers.get("User-Agent", "").strip(): + merged_headers["User-Agent"] = KIMI_CODE_USER_AGENT + merged_provider_config["custom_headers"] = merged_headers + + super().__init__(merged_provider_config, provider_settings) diff --git a/tests/test_anthropic_kimi_code_provider.py b/tests/test_anthropic_kimi_code_provider.py new file mode 100644 index 0000000000..dd4a67132f --- /dev/null +++ b/tests/test_anthropic_kimi_code_provider.py @@ -0,0 +1,63 @@ +import httpx + +import astrbot.core.provider.sources.anthropic_source as anthropic_source +import astrbot.core.provider.sources.kimi_code_source as kimi_code_source + + +class _FakeAsyncAnthropic: + def __init__(self, **kwargs): + self.kwargs = kwargs + + async def close(self): + return None + + +def test_anthropic_provider_injects_custom_headers_into_http_client(monkeypatch): + monkeypatch.setattr(anthropic_source, "AsyncAnthropic", _FakeAsyncAnthropic) + + provider = anthropic_source.ProviderAnthropic( + provider_config={ + "id": "anthropic-test", + "type": "anthropic_chat_completion", + "model": "claude-test", + "key": ["test-key"], + "custom_headers": { + "User-Agent": "custom-agent/1.0", + "X-Test-Header": 123, + }, + }, + provider_settings={}, + ) + + assert provider.custom_headers == { + "User-Agent": "custom-agent/1.0", + "X-Test-Header": "123", + } + assert isinstance(provider.client.kwargs["http_client"], httpx.AsyncClient) + assert provider.client.kwargs["http_client"].headers["User-Agent"] == "custom-agent/1.0" + assert provider.client.kwargs["http_client"].headers["X-Test-Header"] == "123" + + +def test_kimi_code_provider_sets_defaults_and_preserves_custom_headers(monkeypatch): + monkeypatch.setattr(anthropic_source, "AsyncAnthropic", _FakeAsyncAnthropic) + + provider = kimi_code_source.ProviderKimiCode( + provider_config={ + "id": "kimi-code", + "type": "kimi_code_chat_completion", + "key": ["test-key"], + "custom_headers": {"X-Trace-Id": "trace-1"}, + }, + provider_settings={}, + ) + + assert provider.base_url == kimi_code_source.KIMI_CODE_API_BASE + assert provider.get_model() == kimi_code_source.KIMI_CODE_DEFAULT_MODEL + assert provider.custom_headers == { + "User-Agent": kimi_code_source.KIMI_CODE_USER_AGENT, + "X-Trace-Id": "trace-1", + } + assert provider.client.kwargs["http_client"].headers["User-Agent"] == ( + kimi_code_source.KIMI_CODE_USER_AGENT + ) + assert provider.client.kwargs["http_client"].headers["X-Trace-Id"] == "trace-1" From 21f209006ffbaf2b03ebfb82e35920a439e34b14 Mon Sep 17 00:00:00 2001 From: danielsu Date: Wed, 18 Mar 2026 17:12:26 +0800 Subject: [PATCH 2/6] Add icon mapping for Kimi Code provider --- dashboard/src/utils/providerUtils.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dashboard/src/utils/providerUtils.js b/dashboard/src/utils/providerUtils.js index 4bfe3ea6e9..c2ebfeb35a 100644 --- a/dashboard/src/utils/providerUtils.js +++ b/dashboard/src/utils/providerUtils.js @@ -21,6 +21,8 @@ export function getProviderIcon(type) { 'nvidia': 'https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@latest/icons/nvidia-color.svg', 'siliconflow': 'https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@latest/icons/siliconcloud.svg', 'moonshot': 'https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@latest/icons/kimi.svg', + 'kimi': 'https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@latest/icons/kimi.svg', + 'kimi-code': 'https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@latest/icons/kimi.svg', 'ppio': 'https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@latest/icons/ppio.svg', 'dify': 'https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@latest/icons/dify-color.svg', "coze": "https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@1.66.0/icons/coze.svg", From a8fc22ceaf065ad5d5bc472d05cf0761c7671df3 Mon Sep 17 00:00:00 2001 From: danielsu Date: Wed, 18 Mar 2026 17:16:00 +0800 Subject: [PATCH 3/6] Clarify Kimi CodingPlan provider labeling --- astrbot/core/config/default.py | 2 +- dashboard/src/i18n/locales/en-US/features/provider.json | 1 + dashboard/src/i18n/locales/ru-RU/features/provider.json | 3 ++- dashboard/src/i18n/locales/zh-CN/features/provider.json | 1 + dashboard/src/utils/providerUtils.js | 2 ++ 5 files changed, 7 insertions(+), 2 deletions(-) diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index ce82fe4cdc..66c95d46d5 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -1115,7 +1115,7 @@ class ChatProviderTemplate(TypedDict): "custom_headers": {}, "anth_thinking_config": {"type": "", "budget": 0, "effort": ""}, }, - "Kimi Code": { + "Kimi CodingPlan": { "id": "kimi-code", "provider": "kimi-code", "type": "kimi_code_chat_completion", diff --git a/dashboard/src/i18n/locales/en-US/features/provider.json b/dashboard/src/i18n/locales/en-US/features/provider.json index 87e26e2253..1dca1a4a55 100644 --- a/dashboard/src/i18n/locales/en-US/features/provider.json +++ b/dashboard/src/i18n/locales/en-US/features/provider.json @@ -21,6 +21,7 @@ }, "description": { "openai": "Supports all OpenAI API compatible providers.", + "kimi_code": "Dedicated Kimi CodingPlan / Kimi Code integration using a custom Anthropic API compatibility layer.", "default": "" } }, diff --git a/dashboard/src/i18n/locales/ru-RU/features/provider.json b/dashboard/src/i18n/locales/ru-RU/features/provider.json index 1287bfbd47..9bc9f31edd 100644 --- a/dashboard/src/i18n/locales/ru-RU/features/provider.json +++ b/dashboard/src/i18n/locales/ru-RU/features/provider.json @@ -21,6 +21,7 @@ }, "description": { "openai": "Поддерживаются все провайдеры, совместимые с OpenAI API.", + "kimi_code": "Специальная интеграция Kimi CodingPlan / Kimi Code с отдельной совместимой адаптацией Anthropic API.", "vllm_rerank": "Также поддерживает Jina AI, Cohere, PPIO и другие.", "default": "Преобразование речи в текст" } @@ -148,4 +149,4 @@ "modelId": "ID модели" } } -} \ No newline at end of file +} diff --git a/dashboard/src/i18n/locales/zh-CN/features/provider.json b/dashboard/src/i18n/locales/zh-CN/features/provider.json index d3f5d95b1d..7a8a1a4d83 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/provider.json +++ b/dashboard/src/i18n/locales/zh-CN/features/provider.json @@ -21,6 +21,7 @@ }, "description": { "openai": "也支持所有兼容 OpenAI API 的模型提供商。", + "kimi_code": "Kimi 的 CodingPlan / Kimi Code 专用接入,使用特殊的 Anthropic API 兼容适配。", "vllm_rerank": "也支持 Jina AI, Cohere, PPIO 等提供商。", "default": "" } diff --git a/dashboard/src/utils/providerUtils.js b/dashboard/src/utils/providerUtils.js index c2ebfeb35a..060df6f5ce 100644 --- a/dashboard/src/utils/providerUtils.js +++ b/dashboard/src/utils/providerUtils.js @@ -54,6 +54,8 @@ export function getProviderIcon(type) { export function getProviderDescription(template, name, tm) { if (name == 'OpenAI') { return tm('providers.description.openai', { type: template.type }); + } else if (name == 'Kimi CodingPlan' || template.provider === 'kimi-code') { + return tm('providers.description.kimi_code'); } else if (name == 'vLLM Rerank') { return tm('providers.description.vllm_rerank', { type: template.type }); } From df267ae5a3bf97f4a982b4be660d02653e91a635 Mon Sep 17 00:00:00 2001 From: danielsu Date: Wed, 18 Mar 2026 17:43:46 +0800 Subject: [PATCH 4/6] Refine Kimi Code header handling --- .../core/provider/sources/anthropic_source.py | 25 +++++++++++++------ .../core/provider/sources/kimi_code_source.py | 12 +++------ dashboard/src/utils/providerUtils.js | 6 ++--- tests/test_anthropic_kimi_code_provider.py | 18 +++++++++++++ 4 files changed, 43 insertions(+), 18 deletions(-) diff --git a/astrbot/core/provider/sources/anthropic_source.py b/astrbot/core/provider/sources/anthropic_source.py index 48fdea81fc..203d0610ff 100644 --- a/astrbot/core/provider/sources/anthropic_source.py +++ b/astrbot/core/provider/sources/anthropic_source.py @@ -16,7 +16,6 @@ from astrbot.core.provider.func_tool_manager import ToolSet from astrbot.core.utils.io import download_image_by_url from astrbot.core.utils.network_utils import ( - create_proxy_client, is_connection_error, log_connection_failure, ) @@ -39,6 +38,20 @@ def _normalize_custom_headers(provider_config: dict) -> dict[str, str] | None: normalized_headers[str(key)] = str(value) return normalized_headers or None + @classmethod + def _resolve_custom_headers( + cls, + provider_config: dict, + *, + required_headers: dict[str, str] | None = None, + ) -> dict[str, str] | None: + merged_headers = cls._normalize_custom_headers(provider_config) or {} + if required_headers: + for header_name, header_value in required_headers.items(): + if not merged_headers.get(header_name, "").strip(): + merged_headers[header_name] = header_value + return merged_headers or None + def __init__( self, provider_config, @@ -56,7 +69,7 @@ def __init__( if isinstance(self.timeout, str): self.timeout = int(self.timeout) self.thinking_config = provider_config.get("anth_thinking_config", {}) - self.custom_headers = self._normalize_custom_headers(provider_config) + self.custom_headers = self._resolve_custom_headers(provider_config) if use_api_key: self._init_api_key(provider_config) @@ -77,11 +90,9 @@ def _init_api_key(self, provider_config: dict) -> None: def _create_http_client(self, provider_config: dict) -> httpx.AsyncClient | None: """创建带代理的 HTTP 客户端""" proxy = provider_config.get("proxy", "") - client = create_proxy_client("Anthropic", proxy) - if client is not None: - if self.custom_headers: - client.headers.update(self.custom_headers) - return client + if proxy: + logger.info(f"[Anthropic] 使用代理: {proxy}") + return httpx.AsyncClient(proxy=proxy, headers=self.custom_headers) if self.custom_headers: return httpx.AsyncClient(headers=self.custom_headers) return None diff --git a/astrbot/core/provider/sources/kimi_code_source.py b/astrbot/core/provider/sources/kimi_code_source.py index 2f76676678..2c94346750 100644 --- a/astrbot/core/provider/sources/kimi_code_source.py +++ b/astrbot/core/provider/sources/kimi_code_source.py @@ -19,13 +19,9 @@ def __init__( merged_provider_config = dict(provider_config) merged_provider_config.setdefault("api_base", KIMI_CODE_API_BASE) merged_provider_config.setdefault("model", KIMI_CODE_DEFAULT_MODEL) - - custom_headers = merged_provider_config.get("custom_headers", {}) - if not isinstance(custom_headers, dict): - custom_headers = {} - merged_headers = {str(key): str(value) for key, value in custom_headers.items()} - if not merged_headers.get("User-Agent", "").strip(): - merged_headers["User-Agent"] = KIMI_CODE_USER_AGENT - merged_provider_config["custom_headers"] = merged_headers + merged_provider_config["custom_headers"] = self._resolve_custom_headers( + merged_provider_config, + required_headers={"User-Agent": KIMI_CODE_USER_AGENT}, + ) super().__init__(merged_provider_config, provider_settings) diff --git a/dashboard/src/utils/providerUtils.js b/dashboard/src/utils/providerUtils.js index 060df6f5ce..7c992f531d 100644 --- a/dashboard/src/utils/providerUtils.js +++ b/dashboard/src/utils/providerUtils.js @@ -52,11 +52,11 @@ export function getProviderIcon(type) { * @returns {string} 提供商描述 */ export function getProviderDescription(template, name, tm) { - if (name == 'OpenAI') { + if (name === 'OpenAI') { return tm('providers.description.openai', { type: template.type }); - } else if (name == 'Kimi CodingPlan' || template.provider === 'kimi-code') { + } else if (template.provider === 'kimi-code') { return tm('providers.description.kimi_code'); - } else if (name == 'vLLM Rerank') { + } else if (name === 'vLLM Rerank') { return tm('providers.description.vllm_rerank', { type: template.type }); } return tm('providers.description.default', { type: template.type }); diff --git a/tests/test_anthropic_kimi_code_provider.py b/tests/test_anthropic_kimi_code_provider.py index dd4a67132f..ea22e376c3 100644 --- a/tests/test_anthropic_kimi_code_provider.py +++ b/tests/test_anthropic_kimi_code_provider.py @@ -61,3 +61,21 @@ def test_kimi_code_provider_sets_defaults_and_preserves_custom_headers(monkeypat kimi_code_source.KIMI_CODE_USER_AGENT ) assert provider.client.kwargs["http_client"].headers["X-Trace-Id"] == "trace-1" + + +def test_kimi_code_provider_restores_required_user_agent_when_blank(monkeypatch): + monkeypatch.setattr(anthropic_source, "AsyncAnthropic", _FakeAsyncAnthropic) + + provider = kimi_code_source.ProviderKimiCode( + provider_config={ + "id": "kimi-code", + "type": "kimi_code_chat_completion", + "key": ["test-key"], + "custom_headers": {"User-Agent": " "}, + }, + provider_settings={}, + ) + + assert provider.custom_headers == { + "User-Agent": kimi_code_source.KIMI_CODE_USER_AGENT, + } From 94f83c438db658b901bb46bc2d0d7d5100b41849 Mon Sep 17 00:00:00 2001 From: daniel5u Date: Wed, 18 Mar 2026 19:25:26 +0800 Subject: [PATCH 5/6] modified docker compose --- compose-with-shipyard.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/compose-with-shipyard.yml b/compose-with-shipyard.yml index 24ced5a95b..7703293fa7 100644 --- a/compose-with-shipyard.yml +++ b/compose-with-shipyard.yml @@ -4,7 +4,10 @@ version: '3.8' services: astrbot: - image: soulter/astrbot:latest + build: + context: . + dockerfile: Dockerfile + image: astrbot:kimi-code container_name: astrbot restart: always ports: # mappings description: https://github.com/AstrBotDevs/AstrBot/issues/497 From 21a947ff5a402f262ea59d925db90c23cbe76383 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Fri, 20 Mar 2026 13:30:50 +0800 Subject: [PATCH 6/6] fix: correct Kimi Coding Plan label and update API base URL --- astrbot/core/config/default.py | 2 +- astrbot/core/provider/sources/kimi_code_source.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 66c95d46d5..3a71325f87 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -1115,7 +1115,7 @@ class ChatProviderTemplate(TypedDict): "custom_headers": {}, "anth_thinking_config": {"type": "", "budget": 0, "effort": ""}, }, - "Kimi CodingPlan": { + "Kimi Coding Plan": { "id": "kimi-code", "provider": "kimi-code", "type": "kimi_code_chat_completion", diff --git a/astrbot/core/provider/sources/kimi_code_source.py b/astrbot/core/provider/sources/kimi_code_source.py index 2c94346750..02c200271f 100644 --- a/astrbot/core/provider/sources/kimi_code_source.py +++ b/astrbot/core/provider/sources/kimi_code_source.py @@ -1,14 +1,14 @@ from ..register import register_provider_adapter from .anthropic_source import ProviderAnthropic -KIMI_CODE_API_BASE = "https://api.kimi.com/coding/" -KIMI_CODE_DEFAULT_MODEL = "kimi-code" +KIMI_CODE_API_BASE = "https://api.kimi.com/coding" +KIMI_CODE_DEFAULT_MODEL = "kimi-for-coding" KIMI_CODE_USER_AGENT = "claude-code/0.1.0" @register_provider_adapter( "kimi_code_chat_completion", - "Kimi Code Chat Completion 提供商适配器", + "Kimi Code Provider Adapter", ) class ProviderKimiCode(ProviderAnthropic): def __init__(