Unlike the OpenAI ChatGPT OAuth flow, Claude Code OAuth piggybacks on
+ * the user's locally-installed Claude Code client — credentials live in the
+ * macOS Keychain or {@code ~/.claude/.credentials.json}, not in
+ * {@code mate_model_provider}. So this controller is read-only:
+ *
+ *
+ *
{@code GET /status} — re-reads from disk and reports
+ * connected / expired / expiry timestamp / source.
+ *
{@code POST /reload} — alias of status, semantically signals
+ * "I just logged in via Claude Code, please re-detect" so the UI can
+ * refresh state without polling. Also triggers an auto-refresh of the
+ * access token when it's near expiry, so the user can verify end-to-end
+ * token validity from the management page.
+ *
+ *
+ *
The PKCE-based in-app login flow (without requiring local Claude Code)
+ * lands in PR-4 and will add {@code /authorize} + {@code /callback-paste}
+ * endpoints here.
+ */
+@Slf4j
+@Tag(name = "Anthropic Claude Code OAuth")
+@RestController
+@RequestMapping("/api/v1/oauth/anthropic")
+@RequiredArgsConstructor
+public class ClaudeCodeOAuthController {
+
+ private final ClaudeCodeOAuthService oauthService;
+
+ @Operation(summary = "Read current Claude Code OAuth credential status from local disk")
+ @GetMapping("/status")
+ public R status() {
+ return R.ok(oauthService.getStatus());
+ }
+
+ /**
+ * Re-detect credentials and force a refresh-if-near-expiry. Returns the
+ * post-action status so the UI can update without a follow-up GET.
+ *
+ *
If no credentials exist or refresh fails, the underlying exception is
+ * caught here and surfaced as {@code connected=false} status — the UI
+ * shouldn't show a red banner just because the user hasn't logged into
+ * Claude Code yet.
+ */
+ @Operation(summary = "Force re-detect credentials and refresh if near expiry")
+ @PostMapping("/reload")
+ public R reload() {
+ try {
+ oauthService.getValidToken();
+ } catch (Exception e) {
+ // Expected when not logged in or refresh upstream is down.
+ // Status response carries enough detail (connected/expired/source)
+ // for the UI to decide what to render.
+ log.debug("[ClaudeCodeOAuth] reload encountered: {}", e.getMessage());
+ }
+ return R.ok(oauthService.getStatus());
+ }
+}
diff --git a/mateclaw-server/src/main/java/vip/mate/llm/service/ModelProviderService.java b/mateclaw-server/src/main/java/vip/mate/llm/service/ModelProviderService.java
index 8b148ad2..5e494d53 100644
--- a/mateclaw-server/src/main/java/vip/mate/llm/service/ModelProviderService.java
+++ b/mateclaw-server/src/main/java/vip/mate/llm/service/ModelProviderService.java
@@ -4,10 +4,12 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import vip.mate.exception.MateClawException;
+import vip.mate.llm.anthropic.oauth.ClaudeCodeOAuthService;
import vip.mate.llm.event.ModelConfigChangedEvent;
import vip.mate.llm.model.*;
import vip.mate.llm.repository.ModelProviderMapper;
@@ -22,9 +24,14 @@ import java.util.stream.Collectors;
@RequiredArgsConstructor
public class ModelProviderService {
+ /** Provider id whose OAuth token lives on local disk (Keychain / ~/.claude/.credentials.json) instead of the database. */
+ private static final String CLAUDE_CODE_PROVIDER_ID = "anthropic-claude-code";
+
private final ModelProviderMapper modelProviderMapper;
private final ModelConfigService modelConfigService;
private final ApplicationEventPublisher eventPublisher;
+ /** Lazy provider — avoids forcing the bean to exist in test contexts that don't load the anthropic package. */
+ private final ObjectProvider claudeCodeOAuthServiceProvider;
private final ObjectMapper objectMapper = new ObjectMapper();
/** Plugin-registered ChatModel instances: providerId -> ChatModel */
@@ -242,8 +249,24 @@ public class ModelProviderService {
dto.setBaseUrl(provider.getBaseUrl());
dto.setGenerateKwargs(readJson(provider.getGenerateKwargs()));
dto.setAuthType(provider.getAuthType() != null ? provider.getAuthType() : "api_key");
- dto.setOauthConnected(StringUtils.hasText(provider.getOauthAccessToken()));
- dto.setOauthExpiresAt(provider.getOauthExpiresAt());
+ if (CLAUDE_CODE_PROVIDER_ID.equals(provider.getProviderId())) {
+ // Claude Code OAuth credentials live on disk (RFC-062), not in the
+ // mate_model_provider row. Bypass the column lookup and ask the
+ // service directly. Falls back to false if the bean isn't present
+ // (e.g. minimal test contexts).
+ ClaudeCodeOAuthService svc = claudeCodeOAuthServiceProvider.getIfAvailable();
+ if (svc != null) {
+ ClaudeCodeOAuthService.OAuthStatus status = svc.getStatus();
+ dto.setOauthConnected(status.connected() && !status.expired());
+ dto.setOauthExpiresAt(status.expiresAtMs() > 0L ? status.expiresAtMs() : null);
+ } else {
+ dto.setOauthConnected(false);
+ dto.setOauthExpiresAt(null);
+ }
+ } else {
+ dto.setOauthConnected(StringUtils.hasText(provider.getOauthAccessToken()));
+ dto.setOauthExpiresAt(provider.getOauthExpiresAt());
+ }
dto.setFallbackPriority(provider.getFallbackPriority() != null ? provider.getFallbackPriority() : 0);
List builtinModels = new ArrayList<>();
List extraModels = new ArrayList<>();
@@ -278,6 +301,11 @@ public class ModelProviderService {
// OAuth 认证的 provider:检查 OAuth token 是否存在
if ("oauth".equals(provider.getAuthType())) {
+ // Claude Code OAuth (RFC-062) — token lives on disk, not in DB.
+ if (CLAUDE_CODE_PROVIDER_ID.equals(provider.getProviderId())) {
+ ClaudeCodeOAuthService svc = claudeCodeOAuthServiceProvider.getIfAvailable();
+ return svc != null && svc.isLoggedIn();
+ }
return StringUtils.hasText(provider.getOauthAccessToken());
}
diff --git a/mateclaw-server/src/main/resources/db/data-en.sql b/mateclaw-server/src/main/resources/db/data-en.sql
index 4a830259..edc47c3a 100644
--- a/mateclaw-server/src/main/resources/db/data-en.sql
+++ b/mateclaw-server/src/main/resources/db/data-en.sql
@@ -118,6 +118,14 @@ MERGE INTO mate_model_provider (provider_id, name, api_key_prefix, chat_model, a
KEY (provider_id)
VALUES ('openai-chatgpt', 'OpenAI ChatGPT (OAuth)', '', 'ChatGPTChatModel', '', 'https://chatgpt.com/backend-api', '{}', FALSE, FALSE, FALSE, FALSE, TRUE, FALSE, 'oauth', NOW(), NOW());
+-- RFC-062: Anthropic Claude Code OAuth provider. Credentials live on local
+-- disk (Keychain / ~/.claude/.credentials.json), not in this row — leave
+-- api_key + oauth_access_token blank. Bearer-auth requests bypass model
+-- discovery + connection check, hence both FALSE.
+MERGE INTO mate_model_provider (provider_id, name, api_key_prefix, chat_model, api_key, base_url, generate_kwargs, is_custom, is_local, support_model_discovery, support_connection_check, freeze_url, require_api_key, auth_type, create_time, update_time)
+KEY (provider_id)
+VALUES ('anthropic-claude-code', 'Anthropic Claude Code (OAuth)', '', 'ClaudeCodeChatModel', '', 'https://api.anthropic.com', '{}', FALSE, FALSE, FALSE, FALSE, TRUE, FALSE, 'oauth', NOW(), NOW());
+
-- ==================== Local model pre-configs (Ollama, disabled by default) ====================
MERGE INTO mate_model_config (id, name, provider, model_name, description, temperature, max_tokens, top_p, builtin, enabled, is_default, create_time, update_time, deleted)
KEY (id) VALUES
@@ -258,7 +266,10 @@ MERGE INTO mate_model_config (id, name, provider, model_name, description, tempe
(1000000270, 'Claude Opus 4.7', 'anthropic', 'claude-opus-4-7', 'Anthropic Claude Opus 4.7 (xhigh adaptive thinking)', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
(1000000271, 'Claude Sonnet 4.7', 'anthropic', 'claude-sonnet-4-7', 'Anthropic Claude Sonnet 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
(1000000272, 'Claude Opus 4.7', 'openrouter', 'anthropic/claude-opus-4-7', 'Claude Opus 4.7 via OpenRouter', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
-(1000000273, 'Claude Sonnet 4.7', 'openrouter', 'anthropic/claude-sonnet-4-7', 'Claude Sonnet 4.7 via OpenRouter', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0);
+(1000000273, 'Claude Sonnet 4.7', 'openrouter', 'anthropic/claude-sonnet-4-7', 'Claude Sonnet 4.7 via OpenRouter', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
+-- RFC-062: Claude 4.7 via Claude Code OAuth subscription (Pro/Max plan).
+(1000000280, 'Claude Opus 4.7', 'anthropic-claude-code', 'claude-opus-4-7', 'Claude Opus 4.7 via Claude Code Pro/Max subscription', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
+(1000000281, 'Claude Sonnet 4.7', 'anthropic-claude-code', 'claude-sonnet-4-7', 'Claude Sonnet 4.7 via Claude Code Pro/Max subscription', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0);
-- Default system settings
MERGE INTO mate_system_setting (id, setting_key, setting_value, description, create_time, update_time)
diff --git a/mateclaw-server/src/main/resources/db/data-mysql-en.sql b/mateclaw-server/src/main/resources/db/data-mysql-en.sql
index f2578ae4..29e1a1ef 100644
--- a/mateclaw-server/src/main/resources/db/data-mysql-en.sql
+++ b/mateclaw-server/src/main/resources/db/data-mysql-en.sql
@@ -134,6 +134,12 @@ INSERT INTO mate_model_provider (provider_id, name, api_key_prefix, chat_model,
VALUES ('openai-chatgpt', 'OpenAI ChatGPT (OAuth)', '', 'ChatGPTChatModel', '', 'https://chatgpt.com/backend-api', '{}', FALSE, FALSE, FALSE, FALSE, TRUE, FALSE, 'oauth', NOW(), NOW())
ON DUPLICATE KEY UPDATE name=VALUES(name), chat_model=VALUES(chat_model), base_url=VALUES(base_url), auth_type=VALUES(auth_type), update_time=VALUES(update_time);
+-- RFC-062: Anthropic Claude Code OAuth provider. Credentials live on local
+-- disk (Keychain / ~/.claude/.credentials.json), not in this row.
+INSERT INTO mate_model_provider (provider_id, name, api_key_prefix, chat_model, api_key, base_url, generate_kwargs, is_custom, is_local, support_model_discovery, support_connection_check, freeze_url, require_api_key, auth_type, create_time, update_time)
+VALUES ('anthropic-claude-code', 'Anthropic Claude Code (OAuth)', '', 'ClaudeCodeChatModel', '', 'https://api.anthropic.com', '{}', FALSE, FALSE, FALSE, FALSE, TRUE, FALSE, 'oauth', NOW(), NOW())
+ON DUPLICATE KEY UPDATE name=VALUES(name), chat_model=VALUES(chat_model), base_url=VALUES(base_url), auth_type=VALUES(auth_type), update_time=VALUES(update_time);
+
-- ==================== Local model pre-configs (Ollama, disabled by default) ====================
INSERT INTO mate_model_config (id, name, provider, model_name, description, temperature, max_tokens, top_p, builtin, enabled, is_default, create_time, update_time, deleted)
VALUES (1000000300, 'Gemma 3', 'ollama', 'gemma3:latest', 'Google Gemma 3, lightweight and efficient for local inference', 0.7, 4096, 0.8, TRUE, FALSE, FALSE, NOW(), NOW(), 0)
@@ -298,7 +304,10 @@ VALUES
(1000000270, 'Claude Opus 4.7', 'anthropic', 'claude-opus-4-7', 'Anthropic Claude Opus 4.7 (xhigh adaptive thinking)', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
(1000000271, 'Claude Sonnet 4.7', 'anthropic', 'claude-sonnet-4-7', 'Anthropic Claude Sonnet 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
(1000000272, 'Claude Opus 4.7', 'openrouter', 'anthropic/claude-opus-4-7', 'Claude Opus 4.7 via OpenRouter', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
-(1000000273, 'Claude Sonnet 4.7', 'openrouter', 'anthropic/claude-sonnet-4-7', 'Claude Sonnet 4.7 via OpenRouter', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0)
+(1000000273, 'Claude Sonnet 4.7', 'openrouter', 'anthropic/claude-sonnet-4-7', 'Claude Sonnet 4.7 via OpenRouter', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
+-- RFC-062: Claude 4.7 via Claude Code OAuth subscription (Pro/Max plan).
+(1000000280, 'Claude Opus 4.7', 'anthropic-claude-code', 'claude-opus-4-7', 'Claude Opus 4.7 via Claude Code Pro/Max subscription', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
+(1000000281, 'Claude Sonnet 4.7', 'anthropic-claude-code', 'claude-sonnet-4-7', 'Claude Sonnet 4.7 via Claude Code Pro/Max subscription', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0)
ON DUPLICATE KEY UPDATE name=VALUES(name), provider=VALUES(provider), model_name=VALUES(model_name), description=VALUES(description), temperature=VALUES(temperature), max_tokens=VALUES(max_tokens), top_p=VALUES(top_p), builtin=VALUES(builtin), enabled=VALUES(enabled), is_default=VALUES(is_default), update_time=VALUES(update_time), deleted=VALUES(deleted);
-- Default system settings
diff --git a/mateclaw-server/src/main/resources/db/data-mysql-zh.sql b/mateclaw-server/src/main/resources/db/data-mysql-zh.sql
index 6f4a6c02..0fa59483 100644
--- a/mateclaw-server/src/main/resources/db/data-mysql-zh.sql
+++ b/mateclaw-server/src/main/resources/db/data-mysql-zh.sql
@@ -134,6 +134,12 @@ INSERT INTO mate_model_provider (provider_id, name, api_key_prefix, chat_model,
VALUES ('openai-chatgpt', 'OpenAI ChatGPT (OAuth)', '', 'ChatGPTChatModel', '', 'https://chatgpt.com/backend-api', '{}', FALSE, FALSE, FALSE, FALSE, TRUE, FALSE, 'oauth', NOW(), NOW())
ON DUPLICATE KEY UPDATE name=VALUES(name), chat_model=VALUES(chat_model), base_url=VALUES(base_url), auth_type=VALUES(auth_type), update_time=VALUES(update_time);
+-- RFC-062:Anthropic Claude Code OAuth 订阅 provider。凭据存储在本地磁盘
+-- (macOS Keychain 或 ~/.claude/.credentials.json),不写入该行。
+INSERT INTO mate_model_provider (provider_id, name, api_key_prefix, chat_model, api_key, base_url, generate_kwargs, is_custom, is_local, support_model_discovery, support_connection_check, freeze_url, require_api_key, auth_type, create_time, update_time)
+VALUES ('anthropic-claude-code', 'Anthropic Claude Code (OAuth 订阅)', '', 'ClaudeCodeChatModel', '', 'https://api.anthropic.com', '{}', FALSE, FALSE, FALSE, FALSE, TRUE, FALSE, 'oauth', NOW(), NOW())
+ON DUPLICATE KEY UPDATE name=VALUES(name), chat_model=VALUES(chat_model), base_url=VALUES(base_url), auth_type=VALUES(auth_type), update_time=VALUES(update_time);
+
-- ==================== 本地模型预配置(Ollama,默认禁用) ====================
INSERT INTO mate_model_config (id, name, provider, model_name, description, temperature, max_tokens, top_p, builtin, enabled, is_default, create_time, update_time, deleted)
VALUES (1000000300, 'Gemma 3', 'ollama', 'gemma3:latest', 'Google Gemma 3,轻量高效,适合本地推理', 0.7, 4096, 0.8, TRUE, FALSE, FALSE, NOW(), NOW(), 0)
@@ -298,7 +304,10 @@ VALUES
(1000000270, 'Claude Opus 4.7', 'anthropic', 'claude-opus-4-7', 'Anthropic Claude Opus 4.7(xhigh 自适应思考)', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
(1000000271, 'Claude Sonnet 4.7', 'anthropic', 'claude-sonnet-4-7', 'Anthropic Claude Sonnet 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
(1000000272, 'Claude Opus 4.7', 'openrouter', 'anthropic/claude-opus-4-7', 'OpenRouter 代理 Claude Opus 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
-(1000000273, 'Claude Sonnet 4.7', 'openrouter', 'anthropic/claude-sonnet-4-7', 'OpenRouter 代理 Claude Sonnet 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0)
+(1000000273, 'Claude Sonnet 4.7', 'openrouter', 'anthropic/claude-sonnet-4-7', 'OpenRouter 代理 Claude Sonnet 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
+-- RFC-062:通过 Claude Code Pro/Max 订阅调用 Claude 4.7
+(1000000280, 'Claude Opus 4.7', 'anthropic-claude-code', 'claude-opus-4-7', '通过 Claude Code Pro/Max 订阅调用 Claude Opus 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
+(1000000281, 'Claude Sonnet 4.7', 'anthropic-claude-code', 'claude-sonnet-4-7', '通过 Claude Code Pro/Max 订阅调用 Claude Sonnet 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0)
ON DUPLICATE KEY UPDATE name=VALUES(name), provider=VALUES(provider), model_name=VALUES(model_name), description=VALUES(description), temperature=VALUES(temperature), max_tokens=VALUES(max_tokens), top_p=VALUES(top_p), builtin=VALUES(builtin), enabled=VALUES(enabled), is_default=VALUES(is_default), update_time=VALUES(update_time), deleted=VALUES(deleted);
-- 默认系统设置
diff --git a/mateclaw-server/src/main/resources/db/data-zh.sql b/mateclaw-server/src/main/resources/db/data-zh.sql
index 706d161d..791ece92 100644
--- a/mateclaw-server/src/main/resources/db/data-zh.sql
+++ b/mateclaw-server/src/main/resources/db/data-zh.sql
@@ -118,6 +118,12 @@ MERGE INTO mate_model_provider (provider_id, name, api_key_prefix, chat_model, a
KEY (provider_id)
VALUES ('openai-chatgpt', 'OpenAI ChatGPT (OAuth)', '', 'ChatGPTChatModel', '', 'https://chatgpt.com/backend-api', '{}', FALSE, FALSE, FALSE, FALSE, TRUE, FALSE, 'oauth', NOW(), NOW());
+-- RFC-062:Anthropic Claude Code OAuth 订阅 provider。凭据存储在本地磁盘
+-- (macOS Keychain 或 ~/.claude/.credentials.json),不写入该行。
+MERGE INTO mate_model_provider (provider_id, name, api_key_prefix, chat_model, api_key, base_url, generate_kwargs, is_custom, is_local, support_model_discovery, support_connection_check, freeze_url, require_api_key, auth_type, create_time, update_time)
+KEY (provider_id)
+VALUES ('anthropic-claude-code', 'Anthropic Claude Code (OAuth 订阅)', '', 'ClaudeCodeChatModel', '', 'https://api.anthropic.com', '{}', FALSE, FALSE, FALSE, FALSE, TRUE, FALSE, 'oauth', NOW(), NOW());
+
-- ==================== 本地模型预配置(Ollama,默认禁用,用户拉取后启用) ====================
MERGE INTO mate_model_config (id, name, provider, model_name, description, temperature, max_tokens, top_p, builtin, enabled, is_default, create_time, update_time, deleted)
KEY (id)
@@ -264,7 +270,10 @@ MERGE INTO mate_model_config (id, name, provider, model_name, description, tempe
(1000000270, 'Claude Opus 4.7', 'anthropic', 'claude-opus-4-7', 'Anthropic Claude Opus 4.7(xhigh 自适应思考)', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
(1000000271, 'Claude Sonnet 4.7', 'anthropic', 'claude-sonnet-4-7', 'Anthropic Claude Sonnet 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
(1000000272, 'Claude Opus 4.7', 'openrouter', 'anthropic/claude-opus-4-7', 'OpenRouter 代理 Claude Opus 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
-(1000000273, 'Claude Sonnet 4.7', 'openrouter', 'anthropic/claude-sonnet-4-7', 'OpenRouter 代理 Claude Sonnet 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0);
+(1000000273, 'Claude Sonnet 4.7', 'openrouter', 'anthropic/claude-sonnet-4-7', 'OpenRouter 代理 Claude Sonnet 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
+-- RFC-062:通过 Claude Code Pro/Max 订阅调用 Claude 4.7
+(1000000280, 'Claude Opus 4.7', 'anthropic-claude-code', 'claude-opus-4-7', '通过 Claude Code Pro/Max 订阅调用 Claude Opus 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
+(1000000281, 'Claude Sonnet 4.7', 'anthropic-claude-code', 'claude-sonnet-4-7', '通过 Claude Code Pro/Max 订阅调用 Claude Sonnet 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0);
-- 默认系统设置
MERGE INTO mate_system_setting (id, setting_key, setting_value, description, create_time, update_time)
diff --git a/mateclaw-server/src/main/resources/db/migration/h2/V43__rfc062_claude_code_oauth_provider.sql b/mateclaw-server/src/main/resources/db/migration/h2/V43__rfc062_claude_code_oauth_provider.sql
new file mode 100644
index 00000000..86624151
--- /dev/null
+++ b/mateclaw-server/src/main/resources/db/migration/h2/V43__rfc062_claude_code_oauth_provider.sql
@@ -0,0 +1,16 @@
+-- RFC-062: Seed the Anthropic Claude Code OAuth provider + its Claude 4.7
+-- model bindings on existing deployments. New installs already get these
+-- rows from data-{en,zh,mysql-en,mysql-zh}.sql via DatabaseBootstrapRunner;
+-- this migration is for operators upgrading from <= V42.
+--
+-- MERGE INTO is the H2 idempotent upsert; running this twice is a no-op.
+
+MERGE INTO mate_model_provider (provider_id, name, api_key_prefix, chat_model, api_key, base_url, generate_kwargs, is_custom, is_local, support_model_discovery, support_connection_check, freeze_url, require_api_key, auth_type, create_time, update_time)
+KEY (provider_id)
+VALUES ('anthropic-claude-code', 'Anthropic Claude Code (OAuth)', '', 'ClaudeCodeChatModel', '', 'https://api.anthropic.com', '{}', FALSE, FALSE, FALSE, FALSE, TRUE, FALSE, 'oauth', NOW(), NOW());
+
+MERGE INTO mate_model_config (id, name, provider, model_name, description, temperature, max_tokens, top_p, builtin, enabled, is_default, create_time, update_time, deleted)
+KEY (id)
+VALUES
+(1000000280, 'Claude Opus 4.7', 'anthropic-claude-code', 'claude-opus-4-7', 'Claude Opus 4.7 via Claude Code Pro/Max subscription', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
+(1000000281, 'Claude Sonnet 4.7', 'anthropic-claude-code', 'claude-sonnet-4-7', 'Claude Sonnet 4.7 via Claude Code Pro/Max subscription', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0);
diff --git a/mateclaw-server/src/main/resources/db/migration/mysql/V43__rfc062_claude_code_oauth_provider.sql b/mateclaw-server/src/main/resources/db/migration/mysql/V43__rfc062_claude_code_oauth_provider.sql
new file mode 100644
index 00000000..dcd148af
--- /dev/null
+++ b/mateclaw-server/src/main/resources/db/migration/mysql/V43__rfc062_claude_code_oauth_provider.sql
@@ -0,0 +1,34 @@
+-- RFC-062: Seed the Anthropic Claude Code OAuth provider + its Claude 4.7
+-- model bindings on existing deployments. New installs already get these
+-- rows from data-mysql-{en,zh}.sql via DatabaseBootstrapRunner; this
+-- migration is for operators upgrading from <= V42.
+--
+-- INSERT ... ON DUPLICATE KEY UPDATE is the MySQL idempotent upsert.
+-- Same V number is used in h2/ for cross-dialect parity.
+
+INSERT INTO mate_model_provider (provider_id, name, api_key_prefix, chat_model, api_key, base_url, generate_kwargs, is_custom, is_local, support_model_discovery, support_connection_check, freeze_url, require_api_key, auth_type, create_time, update_time)
+VALUES ('anthropic-claude-code', 'Anthropic Claude Code (OAuth)', '', 'ClaudeCodeChatModel', '', 'https://api.anthropic.com', '{}', FALSE, FALSE, FALSE, FALSE, TRUE, FALSE, 'oauth', NOW(), NOW())
+ON DUPLICATE KEY UPDATE
+ name = VALUES(name),
+ chat_model = VALUES(chat_model),
+ base_url = VALUES(base_url),
+ auth_type = VALUES(auth_type),
+ update_time = VALUES(update_time);
+
+INSERT INTO mate_model_config (id, name, provider, model_name, description, temperature, max_tokens, top_p, builtin, enabled, is_default, create_time, update_time, deleted)
+VALUES
+(1000000280, 'Claude Opus 4.7', 'anthropic-claude-code', 'claude-opus-4-7', 'Claude Opus 4.7 via Claude Code Pro/Max subscription', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
+(1000000281, 'Claude Sonnet 4.7', 'anthropic-claude-code', 'claude-sonnet-4-7', 'Claude Sonnet 4.7 via Claude Code Pro/Max subscription', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0)
+ON DUPLICATE KEY UPDATE
+ name = VALUES(name),
+ provider = VALUES(provider),
+ model_name = VALUES(model_name),
+ description = VALUES(description),
+ temperature = VALUES(temperature),
+ max_tokens = VALUES(max_tokens),
+ top_p = VALUES(top_p),
+ builtin = VALUES(builtin),
+ enabled = VALUES(enabled),
+ is_default = VALUES(is_default),
+ update_time = VALUES(update_time),
+ deleted = VALUES(deleted);
diff --git a/mateclaw-ui/src/api/index.ts b/mateclaw-ui/src/api/index.ts
index 6d664134..992f05f4 100644
--- a/mateclaw-ui/src/api/index.ts
+++ b/mateclaw-ui/src/api/index.ts
@@ -330,6 +330,14 @@ export const oauthApi = {
revoke: () => http.delete('/oauth/openai/revoke'),
}
+// RFC-062: Claude Code OAuth piggybacks on the user's local Claude Code
+// install — no in-app authorize/revoke flow yet (PR-4). Until then the UI
+// can only check status + force a re-detect from disk.
+export const claudeCodeOAuthApi = {
+ status: () => http.get('/oauth/anthropic/status'),
+ reload: () => http.post('/oauth/anthropic/reload'),
+}
+
// ==================== Setup ====================
export const setupApi = {
onboardingStatus: () => http.get('/setup/onboarding-status'),
diff --git a/mateclaw-ui/src/i18n/locales/en-US.ts b/mateclaw-ui/src/i18n/locales/en-US.ts
index cf4b498b..9ba3e05d 100644
--- a/mateclaw-ui/src/i18n/locales/en-US.ts
+++ b/mateclaw-ui/src/i18n/locales/en-US.ts
@@ -363,6 +363,11 @@ export default {
oauthHint: 'Sign in with your OpenAI account to use ChatGPT Plus/Pro member quota (not API credits).',
oauthLoginSuccess: 'OpenAI OAuth login successful',
oauthRevokeSuccess: 'OpenAI OAuth disconnected',
+ // RFC-062: Claude Code OAuth (subscription piggyback)
+ claudeCodeOauthDetect: 'Detect Claude Code Login',
+ claudeCodeOauthHint: 'Reuses your local Claude Code Pro/Max subscription. Sign in via the Claude Code app first, then click "Detect" to pick up the credentials.',
+ claudeCodeOauthInstructions: 'No Claude Code credentials found. Install Claude Code, sign in with a Pro/Max account, then click Detect again.',
+ claudeCodeOauthRevokeHint: 'Sign out from the Claude Code app to revoke. MateClaw does not modify Claude Code\'s on-disk credentials.',
fields: {
providerId: 'Provider ID',
providerName: 'Provider Name',
diff --git a/mateclaw-ui/src/i18n/locales/zh-CN.ts b/mateclaw-ui/src/i18n/locales/zh-CN.ts
index 1f199b9f..09e2f213 100644
--- a/mateclaw-ui/src/i18n/locales/zh-CN.ts
+++ b/mateclaw-ui/src/i18n/locales/zh-CN.ts
@@ -353,6 +353,11 @@ export default {
oauthHint: '通过 OAuth 登录 OpenAI 账号,使用 ChatGPT Plus/Pro 会员额度(非 API 额度)。',
oauthLoginSuccess: 'OpenAI OAuth 登录成功',
oauthRevokeSuccess: '已断开 OpenAI OAuth 连接',
+ // RFC-062:Claude Code OAuth(订阅复用)
+ claudeCodeOauthDetect: '检测 Claude Code 登录态',
+ claudeCodeOauthHint: '复用本地 Claude Code Pro/Max 订阅。请先在 Claude Code 客户端中登录,再点击"检测"读取凭据。',
+ claudeCodeOauthInstructions: '未检测到 Claude Code 凭据。请安装 Claude Code 客户端,使用 Pro/Max 账号登录后再点击检测。',
+ claudeCodeOauthRevokeHint: '请在 Claude Code 客户端中退出登录。MateClaw 不会修改 Claude Code 的本地凭据。',
fields: {
providerId: 'Provider ID',
providerName: 'Provider 名称',
diff --git a/mateclaw-ui/src/views/Settings/Models/modals/ProviderConfigModal.vue b/mateclaw-ui/src/views/Settings/Models/modals/ProviderConfigModal.vue
index 2a494681..92addb85 100644
--- a/mateclaw-ui/src/views/Settings/Models/modals/ProviderConfigModal.vue
+++ b/mateclaw-ui/src/views/Settings/Models/modals/ProviderConfigModal.vue
@@ -38,20 +38,26 @@
v-if="!editingProvider?.oauthConnected"
class="btn-oauth"
type="button"
- @click="$emit('oauthLogin')"
+ @click="$emit('oauthLogin', editingProvider?.id)"
>
- {{ t('settings.model.oauthLogin') }}
+ {{ editingProvider?.id === 'anthropic-claude-code'
+ ? t('settings.model.claudeCodeOauthDetect')
+ : t('settings.model.oauthLogin') }}
-