Skip to content
Open
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
32 changes: 28 additions & 4 deletions astrbot/core/config/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -978,7 +978,19 @@ class ChatProviderTemplate(TypedDict):
"api_base": "https://api.anthropic.com/v1",
"timeout": 120,
"proxy": "",
"anth_thinking_config": {"budget": 0},
"anth_thinking_config": {"type": "", "budget": 0, "effort": ""},
},
"Anthropic (Claude Code OAuth)": {
"id": "anthropic_claude_code_oauth",
"provider": "anthropic",
"type": "anthropic_oauth",
"provider_type": "chat_completion",
"enable": True,
"api_base": "https://api.anthropic.com",
"timeout": 120,
"proxy": "",
"anth_thinking_config": {"type": "", "budget": 0, "effort": ""},
"key": [],
},
"Moonshot": {
"id": "moonshot",
Expand Down Expand Up @@ -1939,13 +1951,25 @@ class ChatProviderTemplate(TypedDict):
},
},
"anth_thinking_config": {
"description": "Thinking Config",
"description": "思考配置",
"type": "object",
"items": {
"type": {
"description": "思考类型",
"type": "string",
"options": ["", "adaptive"],
"hint": "Opus 4.6+ / Sonnet 4.6+ 推荐设为 'adaptive'。留空则使用手动 budget 模式。参见: https://platform.claude.com/docs/en/build-with-claude/adaptive-thinking",
},
"budget": {
"description": "Thinking Budget",
"description": "思考预算",
"type": "int",
"hint": "Anthropic thinking.budget_tokens param. Must >= 1024. See: https://platform.claude.com/docs/en/build-with-claude/extended-thinking",
"hint": "手动 budget_tokens,需 >= 1024。仅在 type 为空时生效。Opus 4.6 / Sonnet 4.6 上已弃用。参见: https://platform.claude.com/docs/en/build-with-claude/extended-thinking",
},
"effort": {
"description": "思考深度",
"type": "string",
"options": ["", "low", "medium", "high", "max"],
"hint": "type 为 'adaptive' 时控制思考深度。默认 'high'。'max' 仅限 Opus 4.6。参见: https://platform.claude.com/docs/en/build-with-claude/effort",
},
},
},
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 @@ -299,6 +299,10 @@ def dynamic_import_provider(self, type: str) -> None:
from .sources.anthropic_source import (
ProviderAnthropic as ProviderAnthropic,
)
case "anthropic_oauth":
from .sources.anthropic_oauth_source import (
ProviderAnthropicOAuth as ProviderAnthropicOAuth,
)
case "googlegenai_chat_completion":
from .sources.gemini_source import (
ProviderGoogleGenAI as ProviderGoogleGenAI,
Expand Down
140 changes: 140 additions & 0 deletions astrbot/core/provider/sources/anthropic_oauth_source.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
from collections.abc import AsyncGenerator

from anthropic import AsyncAnthropic

from astrbot.core.provider.entities import LLMResponse

from ..register import register_provider_adapter
from .anthropic_source import ProviderAnthropic

_OAUTH_DEFAULT_HEADERS = {
"anthropic-beta": "claude-code-20250219,oauth-2025-04-20,context-1m-2025-08-07",
"user-agent": "claude-cli/1.0.0 (external, cli)",
"x-app": "cli",
"anthropic-dangerous-direct-browser-access": "true",
}

_CLAUDE_CODE_SYSTEM_PREFIX = (
"You are Claude Code, Anthropic's official CLI for Claude.\n\n"
)

# 支持 1M 上下文窗口的模型前缀(需配合 context-1m beta header)。
# 新增 4.6+ 模型时需同步更新此列表。
_1M_CONTEXT_MODEL_PREFIXES = (
"claude-opus-4-6",
"claude-sonnet-4-6",
)


@register_provider_adapter(
"anthropic_oauth",
"Anthropic Claude Code OAuth provider adapter",
)
class ProviderAnthropicOAuth(ProviderAnthropic):
def __init__(
self,
provider_config: dict,
provider_settings: dict,
) -> None:
# 禁用父类的 API key 客户端初始化,避免重复构造客户端
super().__init__(provider_config, provider_settings, use_api_key=False)

# 手动解析 key 列表(父类跳过了 _init_api_key)
self.api_keys: list = self.get_keys()
self.chosen_api_key: str = self.api_keys[0] if self.api_keys else ""

# 使用 auth_token(OAuth Bearer 认证)构建客户端
self.client = AsyncAnthropic(
auth_token=self.chosen_api_key,
timeout=self.timeout,
base_url=self.base_url,
default_headers=_OAUTH_DEFAULT_HEADERS,
http_client=self._create_http_client(provider_config),
)

def set_model(self, model_name: str) -> None:
super().set_model(model_name)
if any(model_name.startswith(p) for p in _1M_CONTEXT_MODEL_PREFIXES):
if self.provider_config.get("max_context_tokens", 0) <= 0:
self.provider_config["max_context_tokens"] = 1_000_000

def get_model_metadata_overrides(self, model_ids: list[str]) -> dict[str, dict]:
overrides = {}
for mid in model_ids:
if any(mid.startswith(p) for p in _1M_CONTEXT_MODEL_PREFIXES):
overrides[mid] = {"limit": {"context": 1_000_000}}
return overrides

def set_key(self, key: str) -> None:
self.chosen_api_key = key
# 切换 key 时需要重建客户端以使用新的 auth_token
self.client = AsyncAnthropic(
auth_token=key,
timeout=self.timeout,
base_url=self.base_url,
default_headers=_OAUTH_DEFAULT_HEADERS,
http_client=self._create_http_client(self.provider_config),
)

async def get_models(self) -> list[str]:
return await super().get_models()

async def test(self, timeout: float = 45.0) -> None:
await super().test(timeout)

async def text_chat(
self,
prompt=None,
session_id=None,
image_urls=None,
func_tool=None,
contexts=None,
system_prompt=None,
tool_calls_result=None,
model=None,
extra_user_content_parts=None,
**kwargs,
) -> LLMResponse:
system_prompt = _CLAUDE_CODE_SYSTEM_PREFIX + (system_prompt or "")

return await super().text_chat(
prompt=prompt,
session_id=session_id,
image_urls=image_urls,
func_tool=func_tool,
contexts=contexts,
system_prompt=system_prompt,
tool_calls_result=tool_calls_result,
model=model,
extra_user_content_parts=extra_user_content_parts,
**kwargs,
)

async def text_chat_stream(
self,
prompt=None,
session_id=None,
image_urls=None,
func_tool=None,
contexts=None,
system_prompt=None,
tool_calls_result=None,
model=None,
extra_user_content_parts=None,
**kwargs,
) -> AsyncGenerator[LLMResponse, None]:
system_prompt = _CLAUDE_CODE_SYSTEM_PREFIX + (system_prompt or "")

async for llm_response in super().text_chat_stream(
prompt=prompt,
session_id=session_id,
image_urls=image_urls,
func_tool=func_tool,
contexts=contexts,
system_prompt=system_prompt,
tool_calls_result=tool_calls_result,
model=model,
extra_user_content_parts=extra_user_content_parts,
**kwargs,
):
yield llm_response
47 changes: 30 additions & 17 deletions astrbot/core/provider/sources/anthropic_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,36 +33,57 @@ def __init__(
self,
provider_config,
provider_settings,
*,
use_api_key: bool = True,
) -> None:
super().__init__(
provider_config,
provider_settings,
)

self.chosen_api_key: str = ""
self.api_keys: list = super().get_keys()
self.chosen_api_key = self.api_keys[0] if len(self.api_keys) > 0 else ""
self.base_url = provider_config.get("api_base", "https://api.anthropic.com")
self.timeout = provider_config.get("timeout", 120)
if isinstance(self.timeout, str):
self.timeout = int(self.timeout)
self.thinking_config = provider_config.get("anth_thinking_config", {})

if use_api_key:
self._init_api_key(provider_config)

self.set_model(provider_config.get("model", "unknown"))

def _init_api_key(self, provider_config: dict) -> None:
self.chosen_api_key: str = ""
self.api_keys: list = super().get_keys()
self.chosen_api_key = self.api_keys[0] if len(self.api_keys) > 0 else ""
self.client = AsyncAnthropic(
api_key=self.chosen_api_key,
timeout=self.timeout,
base_url=self.base_url,
http_client=self._create_http_client(provider_config),
)

self.thinking_config = provider_config.get("anth_thinking_config", {})

self.set_model(provider_config.get("model", "unknown"))

def _create_http_client(self, provider_config: dict) -> httpx.AsyncClient | None:
"""创建带代理的 HTTP 客户端"""
proxy = provider_config.get("proxy", "")
return create_proxy_client("Anthropic", proxy)

def _apply_thinking_config(self, payloads: dict) -> None:
thinking_type = self.thinking_config.get("type", "")
if thinking_type == "adaptive":
payloads["thinking"] = {"type": "adaptive"}
effort = self.thinking_config.get("effort", "")
output_cfg = dict(payloads.get("output_config", {}))
if effort:
output_cfg["effort"] = effort
if output_cfg:
payloads["output_config"] = output_cfg
elif not thinking_type and self.thinking_config.get("budget"):
payloads["thinking"] = {
"budget_tokens": self.thinking_config.get("budget"),
"type": "enabled",
}

def _prepare_payload(self, messages: list[dict]):
"""准备 Anthropic API 的请求 payload

Expand Down Expand Up @@ -213,11 +234,7 @@ async def _query(self, payloads: dict, tools: ToolSet | None) -> LLMResponse:

if "max_tokens" not in payloads:
payloads["max_tokens"] = 1024
if self.thinking_config.get("budget"):
payloads["thinking"] = {
"budget_tokens": self.thinking_config.get("budget"),
"type": "enabled",
}
self._apply_thinking_config(payloads)

try:
completion = await self.client.messages.create(
Expand Down Expand Up @@ -287,11 +304,7 @@ async def _query_stream(

if "max_tokens" not in payloads:
payloads["max_tokens"] = 1024
if self.thinking_config.get("budget"):
payloads["thinking"] = {
"budget_tokens": self.thinking_config.get("budget"),
"type": "enabled",
}
self._apply_thinking_config(payloads)

async with self.client.messages.stream(
**payloads, extra_body=extra_body
Expand Down
21 changes: 21 additions & 0 deletions astrbot/dashboard/routes/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,23 @@
MAX_FILE_BYTES = 500 * 1024 * 1024


def _apply_provider_metadata_overrides(
provider: Any, model_ids: list[str], metadata_map: dict
) -> None:
override_fn = getattr(provider, "get_model_metadata_overrides", None)
if not callable(override_fn):
return
overrides_map = override_fn(model_ids) or {}
for mid, overrides in overrides_map.items():
merged = dict(metadata_map.get(mid, {}))
for key, value in overrides.items():
if isinstance(value, dict):
merged[key] = {**merged.get(key, {}), **value}
else:
merged[key] = value
metadata_map[mid] = merged


def try_cast(value: Any, type_: str):
if type_ == "int":
try:
Expand Down Expand Up @@ -727,6 +744,8 @@ async def get_provider_model_list(self):
if meta:
metadata_map[model_id] = meta

_apply_provider_metadata_overrides(provider, models, metadata_map)

ret = {
"models": models,
"provider_id": provider_id,
Expand Down Expand Up @@ -872,6 +891,8 @@ async def get_provider_source_models(self):
if meta:
metadata_map[model_id] = meta

_apply_provider_metadata_overrides(inst, models, metadata_map)

# 销毁实例(如果有 terminate 方法)
terminate_fn = getattr(inst, "terminate", None)
if inspect.iscoroutinefunction(terminate_fn):
Expand Down
4 changes: 3 additions & 1 deletion dashboard/src/composables/useProviderSources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,9 @@ export function useProviderSources(options: UseProviderSourcesOptions) {
// 为 provider source 的 id 字段添加自定义 hint
if (customSchema.provider?.items?.id) {
customSchema.provider.items.id.hint = tm('providerSources.hints.id')
customSchema.provider.items.key.hint = tm('providerSources.hints.key')
customSchema.provider.items.key.hint = editableProviderSource.value?.type === 'anthropic_oauth'
? tm('providerSources.hints.oauthToken')
: tm('providerSources.hints.key')
customSchema.provider.items.api_base.hint = tm('providerSources.hints.apiBase')
}
// 为 proxy 字段添加描述和提示
Expand Down
10 changes: 9 additions & 1 deletion dashboard/src/i18n/locales/en-US/features/config-metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -1174,9 +1174,17 @@
},
"anth_thinking_config": {
"description": "Thinking Config",
"type": {
"description": "Thinking Type",
"hint": "Set 'adaptive' for Opus 4.6+ / Sonnet 4.6+ (recommended). Leave empty to use manual budget mode. See: https://platform.claude.com/docs/en/build-with-claude/adaptive-thinking"
},
"budget": {
"description": "Thinking Budget",
"hint": "Anthropic thinking.budget_tokens param. Must >= 1024. See: https://platform.claude.com/docs/en/build-with-claude/extended-thinking"
"hint": "Anthropic thinking.budget_tokens param. Must >= 1024. Only used when type is empty. Deprecated on Opus 4.6 / Sonnet 4.6. See: https://platform.claude.com/docs/en/build-with-claude/extended-thinking"
},
"effort": {
"description": "Effort Level",
"hint": "Controls thinking depth when type is 'adaptive'. 'high' is the default. 'max' is Opus 4.6 only. See: https://platform.claude.com/docs/en/build-with-claude/effort"
}
},
"minimax-group-id": {
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 @@ -114,6 +114,7 @@
"hints": {
"id": "Provider source ID (not provider ID)",
"key": "API key for authentication",
"oauthToken": "Run `claude setup-token` in your terminal to get a long-lived OAuth token, then paste it here. Token is valid for 1 year.",
"apiBase": "Custom API endpoint URL",
"proxy": "HTTP/HTTPS proxy address, e.g. http://127.0.0.1:7890. Only affects this provider's API requests, doesn't interfere with Docker internal networking."
},
Expand Down
10 changes: 9 additions & 1 deletion dashboard/src/i18n/locales/zh-CN/features/config-metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -1177,9 +1177,17 @@
},
"anth_thinking_config": {
"description": "思考配置",
"type": {
"description": "思考类型",
"hint": "设为 'adaptive' 以使用自适应思考(推荐 Opus 4.6+ / Sonnet 4.6+)。留空则使用手动预算模式。参见: https://platform.claude.com/docs/en/build-with-claude/adaptive-thinking"
},
"budget": {
"description": "思考预算",
"hint": "Anthropic thinking.budget_tokens 参数。必须 >= 1024。参见: https://platform.claude.com/docs/en/build-with-claude/extended-thinking"
"hint": "Anthropic thinking.budget_tokens 参数。必须 >= 1024。仅在思考类型为空时生效。Opus 4.6 / Sonnet 4.6 已弃用。参见: https://platform.claude.com/docs/en/build-with-claude/extended-thinking"
},
"effort": {
"description": "思考深度",
"hint": "当思考类型为 'adaptive' 时控制思考深度。'high' 为默认值。'max' 仅限 Opus 4.6。参见: https://platform.claude.com/docs/en/build-with-claude/effort"
}
},
"minimax-group-id": {
Expand Down
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 @@ -115,6 +115,7 @@
"hints": {
"id": "提供商源唯一 ID(不是提供商 ID)",
"key": "API 密钥",
"oauthToken": "在终端运行 `claude setup-token` 获取长期有效的 OAuth Token,然后粘贴到此处。Token 有效期为 1 年。",
"apiBase": "自定义 API 端点 URL",
"proxy": "HTTP/HTTPS 代理地址,格式如 http://127.0.0.1:7890。仅对该提供商的 API 请求生效,不影响 Docker 内网通信。"
},
Expand Down