Skip to content

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

Open
Minidoracat wants to merge 3 commits intoAstrBotDevs:masterfrom
Minidoracat:feat/anthropic-claude-code-oauth
Open

feat: add Anthropic Claude Code OAuth provider and adaptive thinking support#5209
Minidoracat wants to merge 3 commits intoAstrBotDevs:masterfrom
Minidoracat:feat/anthropic-claude-code-oauth

Conversation

@Minidoracat
Copy link

@Minidoracat Minidoracat commented Feb 18, 2026

Summary

Add a new Anthropic Claude Code OAuth provider adapter that uses claude setup-token generated long-lived OAuth tokens (valid 1 year) with Bearer authentication, replacing the need for standard API keys.

Key Changes

New Provider: anthropic_oauth (anthropic_oauth_source.py, 137 lines)

  • Extends ProviderAnthropic with use_api_key=False to skip redundant API-key client construction
  • Reuses the standard key field for OAuth tokens (multi-token round-robin rotation supported)
  • Creates the Anthropic client with auth_token= (Bearer auth) instead of api_key=
  • Sends required Claude Code headers (anthropic-beta, user-agent, x-app, anthropic-dangerous-direct-browser-access)
  • Prepends Claude Code system prefix to all requests
  • Auto-sets 1M context window for claude-opus-4-6 / claude-sonnet-4-6 models

Adaptive Thinking Support (anthropic_source.py)

  • Added adaptive thinking type with configurable effort levels (low, medium, high, max)
  • Uses output_config.effort parameter for the new Anthropic API format
  • Added use_api_key constructor parameter to allow subclasses to customize key initialization

Dashboard & i18n

  • Provider template registered with dynamic key field hint: when anthropic_oauth is selected, the hint changes to explain claude setup-token usage
  • i18n entries added for both zh-CN and en-US
  • Generic metadata override helper that merges all dict keys for extensibility

How It Works

  1. User runs claude setup-token in terminal to get a long-lived OAuth token (sk-ant-oat01-...)
  2. User adds "Anthropic (Claude Code OAuth)" provider source in AstrBot Dashboard
  3. User pastes token(s) into the standard API Key field (supports multiple tokens for rate-limit rotation)
  4. AstrBot uses Bearer authentication to access Anthropic API with Claude Code capabilities

Files Changed

File Change
astrbot/core/provider/sources/anthropic_oauth_source.py New — OAuth provider adapter
astrbot/core/provider/sources/anthropic_source.py Added use_api_key param + adaptive thinking
astrbot/core/config/default.py Template + metadata for anthropic_oauth
astrbot/core/provider/manager.py Import registration for anthropic_oauth
astrbot/dashboard/routes/config.py Model metadata override hook
dashboard/src/composables/useProviderSources.ts Dynamic key hint for OAuth type
dashboard/src/i18n/locales/*/features/provider.json OAuth token hint (zh-CN + en-US)
dashboard/src/i18n/locales/*/features/config-metadata.json Thinking config i18n

Supersedes #5175.


摘要

新增 Anthropic Claude Code OAuth 提供商适配器,使用 claude setup-token 生成的长效 OAuth 令牌(有效期 1 年)进行 Bearer 认证,替代标准 API Key。

主要变更

新提供商:anthropic_oauth

  • 继承 ProviderAnthropic,通过 use_api_key=False 跳过冗余的 API Key 客户端构造
  • 复用标准 key 字段存放 OAuth 令牌(支持多组令牌轮询)
  • 使用 auth_token=(Bearer 认证)创建 Anthropic 客户端
  • 发送 Claude Code 必需的请求头(anthropic-betauser-agentx-appanthropic-dangerous-direct-browser-access
  • claude-opus-4-6 / claude-sonnet-4-6 模型自动设置 1M 上下文窗口

自适应思考支持

  • 新增 adaptive 思考类型,支持可配置的思考深度等级(lowmediumhighmax
  • 使用 Anthropic 新 API 格式的 output_config.effort 参数

Dashboard 与 i18n

  • 注册提供商模板,并根据提供商类型动态切换 Key 字段的提示信息
  • 新增中英文 i18n 条目
  • 通用化模型元数据覆盖辅助函数,支持所有字典字段的合并

使用方式

  1. 在终端运行 claude setup-token 获取长效 OAuth 令牌(sk-ant-oat01-...
  2. 在 AstrBot Dashboard 新增「Anthropic (Claude Code OAuth)」提供商源
  3. 将令牌粘贴到标准 API Key 字段(支持多组令牌轮询以应对速率限制)
  4. AstrBot 自动使用 Bearer 认证访问 Anthropic API

运行截图

Dashboard - Anthropic Claude Code OAuth 提供商源配置

@dosubot dosubot bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Feb 18, 2026
@dosubot
Copy link

dosubot bot commented Feb 18, 2026

Related Documentation

Checked 1 published document(s) in 1 knowledge base(s). No updates required.

How did I do? Any feedback?  Join Discord

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我发现了 3 个问题,并且给了一些整体性的反馈:

  • ProviderAnthropicOAuth.__init__ 中,你依然让父类先用 api_key 构造了一个 AsyncAnthropic 客户端,然后立刻用 auth_token 客户端替换它;可以考虑调用 super().__init__(..., use_api_key=False),并只初始化你真正需要的部分(keys、timeout 等),以避免多余的客户端构造。
  • 1M 上下文模型的处理逻辑(set_modelget_model_metadata_overrides 中的 _1M_CONTEXT_MODEL_PREFIXES 检查)是重复的;可以考虑抽一个小的辅助方法(例如 supports_1m_context(model_id)),把这部分逻辑集中到一处,便于将来新增模型时统一维护。
  • 硬编码的 _OAUTH_DEFAULT_HEADERS(特别是 beta 标签和带版本号的标识)可能需要定期更新;你或许可以把它们暴露为配置项,或者至少集中放在某个共享/常量位置,并加上清晰的注释说明何时/如何更新,以避免长期失效。
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `ProviderAnthropicOAuth.__init__`, you're still letting the base class construct an `AsyncAnthropic` client with `api_key` before immediately replacing it with an `auth_token` client; consider calling `super().__init__(..., use_api_key=False)` and initializing only the pieces you need (keys, timeout, etc.) to avoid redundant client construction.
- The 1M-context model handling logic (the `_1M_CONTEXT_MODEL_PREFIXES` checks in both `set_model` and `get_model_metadata_overrides`) is duplicated; consider extracting a small helper (e.g. `supports_1m_context(model_id)`) to keep this logic centralized and easier to update when new models are added.
- The hard-coded `_OAUTH_DEFAULT_HEADERS` (especially the beta tags and versioned identifiers) may need periodic updates; you might want to surface these as configuration or at least group them somewhere shared/constant with a clear comment about when/how to update to avoid them going stale.

## Individual Comments

### Comment 1
<location> `astrbot/dashboard/routes/config.py:43-49` </location>
<code_context>
 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
+    for mid, overrides in override_fn(model_ids).items():
+        merged = dict(metadata_map.get(mid, {}))
+        if "limit" in overrides:
</code_context>

<issue_to_address>
**issue (bug_risk):** 需要防止 `get_model_metadata_overrides` 返回 `None` 或非字典类型,以避免运行时错误。

这里对 `override_fn(model_ids).items()` 的调用假设该函数总是返回一个字典。如果它返回 `None` 或非映射类型,就会抛异常并导致 metadata 路由失败。可以考虑先做归一化处理,例如 `overrides_map = override_fn(model_ids) or {}`,并在迭代前验证它是否为映射类型。
</issue_to_address>

### Comment 2
<location> `astrbot/core/provider/sources/anthropic_source.py:71-85` </location>
<code_context>
         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 self.thinking_config.get("budget"):
+            payloads["thinking"] = {
+                "budget_tokens": self.thinking_config.get("budget"),
</code_context>

<issue_to_address>
**suggestion (bug_risk):**`budget` 的处理方式与“仅在 `type` 为空时才生效”的文档行为保持一致。

当前 schema 说明 `budget` 只应在 `type` 为空时生效,但现在的代码会在 `budget` 为真值且 `type` 不等于 `"adaptive"` 时使用它(包括未知的 `type` 值)。为更好地匹配配置约定并避免意外行为,可以更新条件以要求 `type` 为空,例如使用 `elif not thinking_type and self.thinking_config.get("budget"):`,从而在 `type` 被设置时忽略 `budget````suggestion
    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",
            }
```
</issue_to_address>

### Comment 3
<location> `astrbot/core/provider/sources/anthropic_oauth_source.py:34-42` </location>
<code_context>
+    "Anthropic Claude Code OAuth provider adapter",
+)
+class ProviderAnthropicOAuth(ProviderAnthropic):
+    def __init__(
+        self,
+        provider_config: dict,
+        provider_settings: dict,
+    ) -> None:
+        # 让父类通过 key 字段解析 API keys 列表(支持多组轮询)
+        super().__init__(provider_config, provider_settings)
+
+        # 使用 auth_token 替换父类的 api_key 客户端(OAuth 使用 Bearer 认证)
+        self.client = AsyncAnthropic(
+            auth_token=self.chosen_api_key,
</code_context>

<issue_to_address>
**suggestion:** 在 OAuth provider 中通过关闭基类的 `use_api_key`,避免构造不必要的 API-key 客户端。

当前的 `super().__init__(provider_config, provider_settings)` 会执行 `_init_api_key`,并构造一个基于 API key 的 `AsyncAnthropic` 客户端,而你会立即用 OAuth 客户端将其覆盖。为了避免这一步多余的构造并让意图更加清晰,可以向基类传入 `use_api_key=False`,并在这里显式设置 `chosen_api_key` / key 轮询逻辑(例如通过 `super().get_keys()`),这样 OAuth 路径就不会再实例化未被使用的 API-key 客户端。

建议实现如下:

```python
class ProviderAnthropicOAuth(ProviderAnthropic):
    def __init__(
        self,
        provider_config: dict,
        provider_settings: dict,
    ) -> None:
        # 禁用父类的 API Key 客户端构建,仅复用其通用配置逻辑
        # use_api_key=False 避免在基类中构造多余的 AsyncAnthropic 实例
        super().__init__(provider_config, provider_settings, use_api_key=False)

        # 本地解析 key 字段作为 OAuth 的 auth_token(支持多组轮询)
        raw_keys = provider_config.get("key", []) or []
        if isinstance(raw_keys, str):
            keys = [raw_keys]
        else:
            keys = list(raw_keys)

        # 记录可轮询的 token 列表和当前选中的 token
        self._oauth_tokens = keys
        self._oauth_token_index = 0
        self.chosen_api_key = keys[self._oauth_token_index] if keys else ""

        # 使用 auth_token(OAuth Bearer 认证)构建客户端,而不是基类的 api_key 客户端
        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),
        )

```

如果基类已经提供了类似 `get_keys()` 或 key 轮询的辅助方法,你可能还想:
1. 用该辅助方法(例如 `keys = self.get_keys()`)替代本地的 `raw_keys` 解析;
2. 可选地,把轮询集成进与其他地方相同的机制中(例如通过一个方法在每次请求前推进 `_oauth_token_index` 并更新 `self.chosen_api_key`)。
</issue_to_address>

Sourcery 对开源项目是免费的——如果你觉得这次代码审查有帮助,欢迎分享 ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据反馈改进后续的审查建议。
Original comment in English

Hey - I've found 3 issues, and left some high level feedback:

  • In ProviderAnthropicOAuth.__init__, you're still letting the base class construct an AsyncAnthropic client with api_key before immediately replacing it with an auth_token client; consider calling super().__init__(..., use_api_key=False) and initializing only the pieces you need (keys, timeout, etc.) to avoid redundant client construction.
  • The 1M-context model handling logic (the _1M_CONTEXT_MODEL_PREFIXES checks in both set_model and get_model_metadata_overrides) is duplicated; consider extracting a small helper (e.g. supports_1m_context(model_id)) to keep this logic centralized and easier to update when new models are added.
  • The hard-coded _OAUTH_DEFAULT_HEADERS (especially the beta tags and versioned identifiers) may need periodic updates; you might want to surface these as configuration or at least group them somewhere shared/constant with a clear comment about when/how to update to avoid them going stale.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `ProviderAnthropicOAuth.__init__`, you're still letting the base class construct an `AsyncAnthropic` client with `api_key` before immediately replacing it with an `auth_token` client; consider calling `super().__init__(..., use_api_key=False)` and initializing only the pieces you need (keys, timeout, etc.) to avoid redundant client construction.
- The 1M-context model handling logic (the `_1M_CONTEXT_MODEL_PREFIXES` checks in both `set_model` and `get_model_metadata_overrides`) is duplicated; consider extracting a small helper (e.g. `supports_1m_context(model_id)`) to keep this logic centralized and easier to update when new models are added.
- The hard-coded `_OAUTH_DEFAULT_HEADERS` (especially the beta tags and versioned identifiers) may need periodic updates; you might want to surface these as configuration or at least group them somewhere shared/constant with a clear comment about when/how to update to avoid them going stale.

## Individual Comments

### Comment 1
<location> `astrbot/dashboard/routes/config.py:43-49` </location>
<code_context>
 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
+    for mid, overrides in override_fn(model_ids).items():
+        merged = dict(metadata_map.get(mid, {}))
+        if "limit" in overrides:
</code_context>

<issue_to_address>
**issue (bug_risk):** Guard against `get_model_metadata_overrides` returning `None` or a non-dict to avoid runtime errors.

This call to `override_fn(model_ids).items()` assumes the function always returns a dict. If it returns `None` or a non-mapping, this will raise and break the metadata route. Consider normalizing, e.g. `overrides_map = override_fn(model_ids) or {}` and validating it’s a mapping before iterating.
</issue_to_address>

### Comment 2
<location> `astrbot/core/provider/sources/anthropic_source.py:71-85` </location>
<code_context>
         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 self.thinking_config.get("budget"):
+            payloads["thinking"] = {
+                "budget_tokens": self.thinking_config.get("budget"),
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Align `budget` handling with the documented behavior of only applying when `type` is empty.

The schema says `budget` should only apply when `type` is empty, but the code currently uses `budget` whenever it’s truthy and `type` is not `"adaptive"` (including unknown types). To better match the config contract and avoid unexpected behavior, update the condition to require an empty `type`, e.g. `elif not thinking_type and self.thinking_config.get("budget"):` so `budget` is ignored whenever `type` is set.

```suggestion
    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",
            }
```
</issue_to_address>

### Comment 3
<location> `astrbot/core/provider/sources/anthropic_oauth_source.py:34-42` </location>
<code_context>
+    "Anthropic Claude Code OAuth provider adapter",
+)
+class ProviderAnthropicOAuth(ProviderAnthropic):
+    def __init__(
+        self,
+        provider_config: dict,
+        provider_settings: dict,
+    ) -> None:
+        # 让父类通过 key 字段解析 API keys 列表(支持多组轮询)
+        super().__init__(provider_config, provider_settings)
+
+        # 使用 auth_token 替换父类的 api_key 客户端(OAuth 使用 Bearer 认证)
+        self.client = AsyncAnthropic(
+            auth_token=self.chosen_api_key,
</code_context>

<issue_to_address>
**suggestion:** Avoid constructing an unnecessary API-key client in the OAuth provider by disabling `use_api_key` in the base class.

`super().__init__(provider_config, provider_settings)` currently runs `_init_api_key` and builds an API-key–based `AsyncAnthropic` client that you immediately overwrite with the OAuth client. To avoid this redundant construction and clarify intent, pass `use_api_key=False` to the base class and explicitly set up `chosen_api_key` / key rotation here (e.g. via `super().get_keys()`), so the OAuth path never instantiates the unused API-key client.

Suggested implementation:

```python
class ProviderAnthropicOAuth(ProviderAnthropic):
    def __init__(
        self,
        provider_config: dict,
        provider_settings: dict,
    ) -> None:
        # 禁用父类的 API Key 客户端构建,仅复用其通用配置逻辑
        # use_api_key=False 避免在基类中构造多余的 AsyncAnthropic 实例
        super().__init__(provider_config, provider_settings, use_api_key=False)

        # 本地解析 key 字段作为 OAuth 的 auth_token(支持多组轮询)
        raw_keys = provider_config.get("key", []) or []
        if isinstance(raw_keys, str):
            keys = [raw_keys]
        else:
            keys = list(raw_keys)

        # 记录可轮询的 token 列表和当前选中的 token
        self._oauth_tokens = keys
        self._oauth_token_index = 0
        self.chosen_api_key = keys[self._oauth_token_index] if keys else ""

        # 使用 auth_token(OAuth Bearer 认证)构建客户端,而不是基类的 api_key 客户端
        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),
        )

```

If the base class already provides helpers like `get_keys()` or key-rotation utilities, you may want to:
1. Replace the local `raw_keys` parsing with a call to that helper (e.g., `keys = self.get_keys()`).
2. Optionally integrate rotation with the same mechanism used elsewhere (e.g., a method that advances `_oauth_token_index` and updates `self.chosen_api_key` before each request).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@dosubot dosubot bot added the area:provider The bug / feature is about AI Provider, Models, LLM Agent, LLM Agent Runner. label Feb 18, 2026
@Minidoracat
Copy link
Author

Thanks for the review! Addressed in 96b7208:

  1. config.py — defensive guard: Added overrides_map = override_fn(model_ids) or {} before .items().
  2. anthropic_source.py — budget condition: Changed to elif not thinking_type and self.thinking_config.get("budget"): to match documented behavior.
  3. anthropic_oauth_source.pyuse_api_key=False: Intentionally not adopted. The parent's _init_api_key() already handles key list parsing, api_keys, chosen_api_key, get_keys(), and get_current_key(). Using use_api_key=False would require duplicating all that logic (~15 extra lines). The cost of one immediately-GC'd AsyncAnthropic instance is negligible compared to the simplicity gain.

@Minidoracat
Copy link
Author

@sourcery-ai review

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我发现了两个问题,并给出了一些总体反馈:

  • ProviderAnthropicOAuth.__init__ 中,你调用 super().__init__ 时没有传入 use_api_key=False,这会导致基类先使用 api_key 构造一个 AsyncAnthropic 客户端,然后立刻用一个基于 auth_token 的客户端替换掉它;在这里传入 use_api_key=False(只依赖 OAuth token)会更符合意图,也能避免多余的客户端构造。
  • dashboard/routes/config.py 里的 _apply_provider_metadata_overrides 辅助函数目前只从 overrides 中合并 limit 字段;如果未来的 overrides 包含其他字段(例如展示名称、能力等),这些字段会被悄然丢弃——可以考虑要么合并所有字段,要么明确文档说明/在实现上强制只支持 limit
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `ProviderAnthropicOAuth.__init__`, you're calling `super().__init__` without `use_api_key=False`, which causes the base class to construct an `AsyncAnthropic` client with `api_key` only to immediately replace it with an `auth_token`-based client; passing `use_api_key=False` (and relying only on the OAuth token) would better reflect the intent and avoid redundant client construction.
- The `_apply_provider_metadata_overrides` helper in `dashboard/routes/config.py` currently only merges the `limit` key from overrides; if future overrides include other fields (e.g., display name, capabilities), they will be silently dropped—consider either merging all keys or explicitly documenting/enforcing that only `limit` is supported.

## Individual Comments

### Comment 1
<location> `astrbot/core/provider/sources/anthropic_oauth_source.py:34-40` </location>
<code_context>
+    "Anthropic Claude Code OAuth provider adapter",
+)
+class ProviderAnthropicOAuth(ProviderAnthropic):
+    def __init__(
+        self,
+        provider_config: dict,
+        provider_settings: dict,
+    ) -> None:
+        # 让父类通过 key 字段解析 API keys 列表(支持多组轮询)
+        super().__init__(provider_config, provider_settings)
+
+        # 使用 auth_token 替换父类的 api_key 客户端(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),
+        )
+
</code_context>

<issue_to_address>
**suggestion:** Avoid double-constructing the Anthropic client in the OAuth subclass.

`ProviderAnthropic.__init__` now accepts `use_api_key` and builds an `AsyncAnthropic` client via `_init_api_key`. This subclass calls `super().__init__(provider_config, provider_settings)` without changing `use_api_key`, so a client is created with `api_key=` and then replaced by one using `auth_token=`. To avoid this redundant construction and keep client initialization clearer, pass `use_api_key=False` here and handle all OAuth-specific client setup in this subclass.

```suggestion
+    def __init__(
+        self,
+        provider_config: dict,
+        provider_settings: dict,
+    ) -> None:
+        # 禁用父类的 API key 客户端初始化,避免重复构造客户端
+        super().__init__(provider_config, provider_settings, use_api_key=False)
```
</issue_to_address>

### Comment 2
<location> `astrbot/dashboard/routes/config.py:43-52` </location>
<code_context>
 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, {}))
+        if "limit" in overrides:
+            merged["limit"] = {**merged.get("limit", {}), **overrides["limit"]}
+        metadata_map[mid] = merged
+
+
</code_context>

<issue_to_address>
**suggestion:** Metadata override helper only merges `limit` and drops other override keys.

`_apply_provider_metadata_overrides` only merges the `limit` key from each override into `metadata_map`; all other keys are ignored. This matches `ProviderAnthropicOAuth.get_model_metadata_overrides` today, but prevents future providers from overriding additional metadata fields without changing this helper. If you expect more fields later, consider merging the entire override dict (treating `limit` specially if needed) so the helper stays extensible.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进后续的评审。
Original comment in English

Hey - I've found 2 issues, and left some high level feedback:

  • In ProviderAnthropicOAuth.__init__, you're calling super().__init__ without use_api_key=False, which causes the base class to construct an AsyncAnthropic client with api_key only to immediately replace it with an auth_token-based client; passing use_api_key=False (and relying only on the OAuth token) would better reflect the intent and avoid redundant client construction.
  • The _apply_provider_metadata_overrides helper in dashboard/routes/config.py currently only merges the limit key from overrides; if future overrides include other fields (e.g., display name, capabilities), they will be silently dropped—consider either merging all keys or explicitly documenting/enforcing that only limit is supported.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `ProviderAnthropicOAuth.__init__`, you're calling `super().__init__` without `use_api_key=False`, which causes the base class to construct an `AsyncAnthropic` client with `api_key` only to immediately replace it with an `auth_token`-based client; passing `use_api_key=False` (and relying only on the OAuth token) would better reflect the intent and avoid redundant client construction.
- The `_apply_provider_metadata_overrides` helper in `dashboard/routes/config.py` currently only merges the `limit` key from overrides; if future overrides include other fields (e.g., display name, capabilities), they will be silently dropped—consider either merging all keys or explicitly documenting/enforcing that only `limit` is supported.

## Individual Comments

### Comment 1
<location> `astrbot/core/provider/sources/anthropic_oauth_source.py:34-40` </location>
<code_context>
+    "Anthropic Claude Code OAuth provider adapter",
+)
+class ProviderAnthropicOAuth(ProviderAnthropic):
+    def __init__(
+        self,
+        provider_config: dict,
+        provider_settings: dict,
+    ) -> None:
+        # 让父类通过 key 字段解析 API keys 列表(支持多组轮询)
+        super().__init__(provider_config, provider_settings)
+
+        # 使用 auth_token 替换父类的 api_key 客户端(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),
+        )
+
</code_context>

<issue_to_address>
**suggestion:** Avoid double-constructing the Anthropic client in the OAuth subclass.

`ProviderAnthropic.__init__` now accepts `use_api_key` and builds an `AsyncAnthropic` client via `_init_api_key`. This subclass calls `super().__init__(provider_config, provider_settings)` without changing `use_api_key`, so a client is created with `api_key=` and then replaced by one using `auth_token=`. To avoid this redundant construction and keep client initialization clearer, pass `use_api_key=False` here and handle all OAuth-specific client setup in this subclass.

```suggestion
+    def __init__(
+        self,
+        provider_config: dict,
+        provider_settings: dict,
+    ) -> None:
+        # 禁用父类的 API key 客户端初始化,避免重复构造客户端
+        super().__init__(provider_config, provider_settings, use_api_key=False)
```
</issue_to_address>

### Comment 2
<location> `astrbot/dashboard/routes/config.py:43-52` </location>
<code_context>
 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, {}))
+        if "limit" in overrides:
+            merged["limit"] = {**merged.get("limit", {}), **overrides["limit"]}
+        metadata_map[mid] = merged
+
+
</code_context>

<issue_to_address>
**suggestion:** Metadata override helper only merges `limit` and drops other override keys.

`_apply_provider_metadata_overrides` only merges the `limit` key from each override into `metadata_map`; all other keys are ignored. This matches `ProviderAnthropicOAuth.get_model_metadata_overrides` today, but prevents future providers from overriding additional metadata fields without changing this helper. If you expect more fields later, consider merging the entire override dict (treating `limit` specially if needed) so the helper stays extensible.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

- 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:provider The bug / feature is about AI Provider, Models, LLM Agent, LLM Agent Runner. size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments