feat: add Anthropic Claude Code OAuth provider and adaptive thinking support (#5209)

* feat: add Anthropic Claude Code OAuth provider and adaptive thinking support

* fix: add defensive guard for metadata overrides and align budget condition with docs

* refactor: adopt sourcery-ai suggestions for OAuth provider

- Use use_api_key=False in OAuth subclass to avoid redundant
  API-key client construction before replacing with auth_token client
- Generalize metadata override helper to merge all dict keys
  instead of only handling 'limit', improving extensibility
This commit is contained in:
Minidoracat
2026-02-21 23:29:15 +08:00
committed by GitHub
parent ae839ef6d8
commit 7b302445c2
10 changed files with 246 additions and 24 deletions
+28 -4
View File
@@ -979,7 +979,19 @@ CONFIG_METADATA_2 = {
"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",
@@ -1964,13 +1976,25 @@ CONFIG_METADATA_2 = {
},
},
"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",
},
},
},
+4
View File
@@ -309,6 +309,10 @@ class ProviderManager:
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,
@@ -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_tokenOAuth 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
@@ -33,20 +33,29 @@ class ProviderAnthropic(Provider):
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,
@@ -54,15 +63,27 @@ class ProviderAnthropic(Provider):
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
@@ -213,11 +234,7 @@ class ProviderAnthropic(Provider):
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(
@@ -287,11 +304,7 @@ class ProviderAnthropic(Provider):
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
+21
View File
@@ -40,6 +40,23 @@ from .util import (
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:
@@ -727,6 +744,8 @@ class ConfigRoute(Route):
if meta:
metadata_map[model_id] = meta
_apply_provider_metadata_overrides(provider, models, metadata_map)
ret = {
"models": models,
"provider_id": provider_id,
@@ -872,6 +891,8 @@ class ConfigRoute(Route):
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):
@@ -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 字段添加描述和提示
@@ -1178,9 +1178,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": {
@@ -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."
},
@@ -1181,9 +1181,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": {
@@ -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 内网通信。"
},