import { formatCliCommand } from "openclaw/plugin-sdk/cli-runtime"; import { parseDurationMs } from "openclaw/plugin-sdk/cli-runtime"; import { definePluginEntry, type ProviderAuthContext, type ProviderResolveDynamicModelContext, type ProviderRuntimeModel, } from "openclaw/plugin-sdk/core"; import { CLAUDE_CLI_PROFILE_ID, applyAuthProfileConfig, buildTokenProfileId, createProviderApiKeyAuthMethod, ensureApiKeyFromOptionEnvOrPrompt, listProfilesForProvider, normalizeApiKeyInput, suggestOAuthProfileIdForLegacyDefault, type AuthProfileStore, type ProviderAuthResult, normalizeSecretInput, normalizeSecretInputModeInput, promptSecretRefForSetup, resolveSecretInputModeForEnvSelection, upsertAuthProfile, validateAnthropicSetupToken, validateApiKeyInput, } from "openclaw/plugin-sdk/provider-auth"; import { normalizeModelCompat } from "openclaw/plugin-sdk/provider-models"; import { fetchClaudeUsage } from "openclaw/plugin-sdk/provider-usage"; import { anthropicMediaUnderstandingProvider } from "./media-understanding-provider.js"; const PROVIDER_ID = "anthropic"; const DEFAULT_ANTHROPIC_MODEL = "anthropic/claude-sonnet-4-6"; const ANTHROPIC_OPUS_46_MODEL_ID = "claude-opus-4-6"; const ANTHROPIC_OPUS_46_DOT_MODEL_ID = "claude-opus-4.6"; const ANTHROPIC_OPUS_TEMPLATE_MODEL_IDS = ["claude-opus-4-5", "claude-opus-4.5"] as const; const ANTHROPIC_SONNET_46_MODEL_ID = "claude-sonnet-4-6"; const ANTHROPIC_SONNET_46_DOT_MODEL_ID = "claude-sonnet-4.6"; const ANTHROPIC_SONNET_TEMPLATE_MODEL_IDS = ["claude-sonnet-4-5", "claude-sonnet-4.5"] as const; const ANTHROPIC_MODERN_MODEL_PREFIXES = [ "claude-opus-4-6", "claude-sonnet-4-6", "claude-opus-4-5", "claude-sonnet-4-5", "claude-haiku-4-5", ] as const; const ANTHROPIC_OAUTH_ALLOWLIST = [ "anthropic/claude-sonnet-4-6", "anthropic/claude-opus-4-6", "anthropic/claude-opus-4-5", "anthropic/claude-sonnet-4-5", "anthropic/claude-haiku-4-5", ] as const; function cloneFirstTemplateModel(params: { modelId: string; templateIds: readonly string[]; ctx: ProviderResolveDynamicModelContext; }): ProviderRuntimeModel | undefined { const trimmedModelId = params.modelId.trim(); for (const templateId of [...new Set(params.templateIds)].filter(Boolean)) { const template = params.ctx.modelRegistry.find( PROVIDER_ID, templateId, ) as ProviderRuntimeModel | null; if (!template) { continue; } return normalizeModelCompat({ ...template, id: trimmedModelId, name: trimmedModelId, } as ProviderRuntimeModel); } return undefined; } function resolveAnthropic46ForwardCompatModel(params: { ctx: ProviderResolveDynamicModelContext; dashModelId: string; dotModelId: string; dashTemplateId: string; dotTemplateId: string; fallbackTemplateIds: readonly string[]; }): ProviderRuntimeModel | undefined { const trimmedModelId = params.ctx.modelId.trim(); const lower = trimmedModelId.toLowerCase(); const is46Model = lower === params.dashModelId || lower === params.dotModelId || lower.startsWith(`${params.dashModelId}-`) || lower.startsWith(`${params.dotModelId}-`); if (!is46Model) { return undefined; } const templateIds: string[] = []; if (lower.startsWith(params.dashModelId)) { templateIds.push(lower.replace(params.dashModelId, params.dashTemplateId)); } if (lower.startsWith(params.dotModelId)) { templateIds.push(lower.replace(params.dotModelId, params.dotTemplateId)); } templateIds.push(...params.fallbackTemplateIds); return cloneFirstTemplateModel({ modelId: trimmedModelId, templateIds, ctx: params.ctx, }); } function resolveAnthropicForwardCompatModel( ctx: ProviderResolveDynamicModelContext, ): ProviderRuntimeModel | undefined { return ( resolveAnthropic46ForwardCompatModel({ ctx, dashModelId: ANTHROPIC_OPUS_46_MODEL_ID, dotModelId: ANTHROPIC_OPUS_46_DOT_MODEL_ID, dashTemplateId: "claude-opus-4-5", dotTemplateId: "claude-opus-4.5", fallbackTemplateIds: ANTHROPIC_OPUS_TEMPLATE_MODEL_IDS, }) ?? resolveAnthropic46ForwardCompatModel({ ctx, dashModelId: ANTHROPIC_SONNET_46_MODEL_ID, dotModelId: ANTHROPIC_SONNET_46_DOT_MODEL_ID, dashTemplateId: "claude-sonnet-4-5", dotTemplateId: "claude-sonnet-4.5", fallbackTemplateIds: ANTHROPIC_SONNET_TEMPLATE_MODEL_IDS, }) ); } function matchesAnthropicModernModel(modelId: string): boolean { const lower = modelId.trim().toLowerCase(); return ANTHROPIC_MODERN_MODEL_PREFIXES.some((prefix) => lower.startsWith(prefix)); } function buildAnthropicAuthDoctorHint(params: { config?: ProviderAuthContext["config"]; store: AuthProfileStore; profileId?: string; }): string { const legacyProfileId = params.profileId ?? "anthropic:default"; const suggested = suggestOAuthProfileIdForLegacyDefault({ cfg: params.config, store: params.store, provider: PROVIDER_ID, legacyProfileId, }); if (!suggested || suggested === legacyProfileId) { return ""; } const storeOauthProfiles = listProfilesForProvider(params.store, PROVIDER_ID) .filter((id) => params.store.profiles[id]?.type === "oauth") .join(", "); const cfgMode = params.config?.auth?.profiles?.[legacyProfileId]?.mode; const cfgProvider = params.config?.auth?.profiles?.[legacyProfileId]?.provider; return [ "Doctor hint (for GitHub issue):", `- provider: ${PROVIDER_ID}`, `- config: ${legacyProfileId}${ cfgProvider || cfgMode ? ` (provider=${cfgProvider ?? "?"}, mode=${cfgMode ?? "?"})` : "" }`, `- auth store oauth profiles: ${storeOauthProfiles || "(none)"}`, `- suggested profile: ${suggested}`, `Fix: run "${formatCliCommand("openclaw doctor --yes")}"`, ].join("\n"); } async function runAnthropicSetupToken(ctx: ProviderAuthContext): Promise { await ctx.prompter.note( ["Run `claude setup-token` in your terminal.", "Then paste the generated token below."].join( "\n", ), "Anthropic setup-token", ); const requestedSecretInputMode = normalizeSecretInputModeInput(ctx.secretInputMode); const selectedMode = ctx.allowSecretRefPrompt ? await resolveSecretInputModeForEnvSelection({ prompter: ctx.prompter, explicitMode: requestedSecretInputMode, copy: { modeMessage: "How do you want to provide this setup token?", plaintextLabel: "Paste setup token now", plaintextHint: "Stores the token directly in the auth profile", }, }) : "plaintext"; let token = ""; let tokenRef: { source: "env" | "file" | "exec"; provider: string; id: string } | undefined; if (selectedMode === "ref") { const resolved = await promptSecretRefForSetup({ provider: "anthropic-setup-token", config: ctx.config, prompter: ctx.prompter, preferredEnvVar: "ANTHROPIC_SETUP_TOKEN", copy: { sourceMessage: "Where is this Anthropic setup token stored?", envVarPlaceholder: "ANTHROPIC_SETUP_TOKEN", }, }); token = resolved.resolvedValue.trim(); tokenRef = resolved.ref; } else { const tokenRaw = await ctx.prompter.text({ message: "Paste Anthropic setup-token", validate: (value) => validateAnthropicSetupToken(String(value ?? "")), }); token = String(tokenRaw ?? "").trim(); } const tokenError = validateAnthropicSetupToken(token); if (tokenError) { throw new Error(tokenError); } const profileNameRaw = await ctx.prompter.text({ message: "Token name (blank = default)", placeholder: "default", }); return { profiles: [ { profileId: buildTokenProfileId({ provider: PROVIDER_ID, name: String(profileNameRaw ?? ""), }), credential: { type: "token", provider: PROVIDER_ID, token, ...(tokenRef ? { tokenRef } : {}), }, }, ], }; } async function runAnthropicSetupTokenNonInteractive(ctx: { config: ProviderAuthContext["config"]; opts: { tokenProvider?: string; token?: string; tokenExpiresIn?: string; tokenProfileId?: string; }; runtime: ProviderAuthContext["runtime"]; agentDir?: string; }): Promise { const provider = ctx.opts.tokenProvider?.trim().toLowerCase(); if (!provider) { ctx.runtime.error("Missing --token-provider for --auth-choice token."); ctx.runtime.exit(1); return null; } if (provider !== PROVIDER_ID) { ctx.runtime.error("Only --token-provider anthropic is supported for --auth-choice token."); ctx.runtime.exit(1); return null; } const token = normalizeSecretInput(ctx.opts.token); if (!token) { ctx.runtime.error("Missing --token for --auth-choice token."); ctx.runtime.exit(1); return null; } const tokenError = validateAnthropicSetupToken(token); if (tokenError) { ctx.runtime.error(tokenError); ctx.runtime.exit(1); return null; } let expires: number | undefined; const expiresInRaw = ctx.opts.tokenExpiresIn?.trim(); if (expiresInRaw) { try { expires = Date.now() + parseDurationMs(expiresInRaw, { defaultUnit: "d" }); } catch (err) { ctx.runtime.error(`Invalid --token-expires-in: ${String(err)}`); ctx.runtime.exit(1); return null; } } const profileId = ctx.opts.tokenProfileId?.trim() || buildTokenProfileId({ provider: PROVIDER_ID, name: "" }); upsertAuthProfile({ profileId, agentDir: ctx.agentDir, credential: { type: "token", provider: PROVIDER_ID, token, ...(expires ? { expires } : {}), }, }); return applyAuthProfileConfig(ctx.config, { profileId, provider: PROVIDER_ID, mode: "token", }); } export default definePluginEntry({ id: PROVIDER_ID, name: "Anthropic Provider", description: "Bundled Anthropic provider plugin", register(api) { api.registerProvider({ id: PROVIDER_ID, label: "Anthropic", docsPath: "/providers/models", envVars: ["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"], deprecatedProfileIds: [CLAUDE_CLI_PROFILE_ID], auth: [ { id: "setup-token", label: "setup-token (claude)", hint: "Paste a setup-token from `claude setup-token`", kind: "token", wizard: { choiceId: "token", choiceLabel: "Anthropic token (paste setup-token)", choiceHint: "Run `claude setup-token` elsewhere, then paste the token here", groupId: "anthropic", groupLabel: "Anthropic", groupHint: "setup-token + API key", modelAllowlist: { allowedKeys: [...ANTHROPIC_OAUTH_ALLOWLIST], initialSelections: ["anthropic/claude-sonnet-4-6"], message: "Anthropic OAuth models", }, }, run: async (ctx: ProviderAuthContext) => await runAnthropicSetupToken(ctx), runNonInteractive: async (ctx) => await runAnthropicSetupTokenNonInteractive({ config: ctx.config, opts: ctx.opts, runtime: ctx.runtime, agentDir: ctx.agentDir, }), }, createProviderApiKeyAuthMethod({ providerId: PROVIDER_ID, methodId: "api-key", label: "Anthropic API key", hint: "Direct Anthropic API key", optionKey: "anthropicApiKey", flagName: "--anthropic-api-key", envVar: "ANTHROPIC_API_KEY", promptMessage: "Enter Anthropic API key", defaultModel: DEFAULT_ANTHROPIC_MODEL, expectedProviders: ["anthropic"], wizard: { choiceId: "apiKey", choiceLabel: "Anthropic API key", groupId: "anthropic", groupLabel: "Anthropic", groupHint: "setup-token + API key", }, }), ], resolveDynamicModel: (ctx) => resolveAnthropicForwardCompatModel(ctx), capabilities: { providerFamily: "anthropic", dropThinkingBlockModelHints: ["claude"], }, isModernModelRef: ({ modelId }) => matchesAnthropicModernModel(modelId), resolveDefaultThinkingLevel: ({ modelId }) => matchesAnthropicModernModel(modelId) && (modelId.toLowerCase().startsWith(ANTHROPIC_OPUS_46_MODEL_ID) || modelId.toLowerCase().startsWith(ANTHROPIC_OPUS_46_DOT_MODEL_ID) || modelId.toLowerCase().startsWith(ANTHROPIC_SONNET_46_MODEL_ID) || modelId.toLowerCase().startsWith(ANTHROPIC_SONNET_46_DOT_MODEL_ID)) ? "adaptive" : undefined, resolveUsageAuth: async (ctx) => await ctx.resolveOAuthToken(), fetchUsageSnapshot: async (ctx) => await fetchClaudeUsage(ctx.token, ctx.timeoutMs, ctx.fetchFn), isCacheTtlEligible: () => true, buildAuthDoctorHint: (ctx) => buildAnthropicAuthDoctorHint({ config: ctx.config, store: ctx.store, profileId: ctx.profileId, }), }); api.registerMediaUnderstandingProvider(anthropicMediaUnderstandingProvider); }, });