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
14 changes: 14 additions & 0 deletions astrbot/core/config/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 Coding Plan": {
"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": {
Expand Down
4 changes: 4 additions & 0 deletions astrbot/core/provider/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
33 changes: 31 additions & 2 deletions astrbot/core/provider/sources/anthropic_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand All @@ -29,6 +28,30 @@
"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

@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,
Expand All @@ -46,6 +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._resolve_custom_headers(provider_config)

if use_api_key:
self._init_api_key(provider_config)
Expand All @@ -66,7 +90,12 @@ 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)
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

def _apply_thinking_config(self, payloads: dict) -> None:
thinking_type = self.thinking_config.get("type", "")
Expand Down
27 changes: 27 additions & 0 deletions astrbot/core/provider/sources/kimi_code_source.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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-for-coding"
KIMI_CODE_USER_AGENT = "claude-code/0.1.0"


@register_provider_adapter(
"kimi_code_chat_completion",
"Kimi Code Provider Adapter",
)
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)
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)
5 changes: 4 additions & 1 deletion compose-with-shipyard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions dashboard/src/i18n/locales/en-US/features/provider.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": ""
}
},
Expand Down
3 changes: 2 additions & 1 deletion dashboard/src/i18n/locales/ru-RU/features/provider.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
},
"description": {
"openai": "Поддерживаются все провайдеры, совместимые с OpenAI API.",
"kimi_code": "Специальная интеграция Kimi CodingPlan / Kimi Code с отдельной совместимой адаптацией Anthropic API.",
"vllm_rerank": "Также поддерживает Jina AI, Cohere, PPIO и другие.",
"default": "Преобразование речи в текст"
}
Expand Down Expand Up @@ -148,4 +149,4 @@
"modelId": "ID модели"
}
}
}
}
1 change: 1 addition & 0 deletions dashboard/src/i18n/locales/zh-CN/features/provider.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
},
"description": {
"openai": "也支持所有兼容 OpenAI API 的模型提供商。",
"kimi_code": "Kimi 的 CodingPlan / Kimi Code 专用接入,使用特殊的 Anthropic API 兼容适配。",
"vllm_rerank": "也支持 Jina AI, Cohere, PPIO 等提供商。",
"default": ""
}
Expand Down
8 changes: 6 additions & 2 deletions dashboard/src/utils/providerUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -50,9 +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 == 'vLLM Rerank') {
} else if (template.provider === 'kimi-code') {
return tm('providers.description.kimi_code');
} else if (name === 'vLLM Rerank') {
return tm('providers.description.vllm_rerank', { type: template.type });
}
return tm('providers.description.default', { type: template.type });
Expand Down
81 changes: 81 additions & 0 deletions tests/test_anthropic_kimi_code_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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"


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,
}
Loading