mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-23 22:55:24 +00:00
refactor(anthropic): lazy-load provider registration
This commit is contained in:
@@ -1,494 +1,11 @@
|
||||
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/plugin-entry";
|
||||
import {
|
||||
CLAUDE_CLI_PROFILE_ID,
|
||||
applyAuthProfileConfig,
|
||||
buildTokenProfileId,
|
||||
ensureApiKeyFromOptionEnvOrPrompt,
|
||||
listProfilesForProvider,
|
||||
normalizeApiKeyInput,
|
||||
suggestOAuthProfileIdForLegacyDefault,
|
||||
type AuthProfileStore,
|
||||
type ProviderAuthResult,
|
||||
normalizeSecretInput,
|
||||
normalizeSecretInputModeInput,
|
||||
promptSecretRefForSetup,
|
||||
resolveSecretInputModeForEnvSelection,
|
||||
upsertAuthProfile,
|
||||
validateAnthropicSetupToken,
|
||||
validateApiKeyInput,
|
||||
} from "openclaw/plugin-sdk/provider-auth";
|
||||
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key";
|
||||
import { cloneFirstTemplateModel } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { composeProviderStreamWrappers } from "openclaw/plugin-sdk/provider-stream";
|
||||
import { fetchClaudeUsage } from "openclaw/plugin-sdk/provider-usage";
|
||||
import { buildAnthropicCliBackend } from "./cli-backend.js";
|
||||
import { buildAnthropicCliMigrationResult, hasClaudeCliAuth } from "./cli-migration.js";
|
||||
import { anthropicMediaUnderstandingProvider } from "./media-understanding-provider.js";
|
||||
import { buildAnthropicReplayPolicy } from "./replay-policy.js";
|
||||
import {
|
||||
createAnthropicBetaHeadersWrapper,
|
||||
createAnthropicFastModeWrapper,
|
||||
createAnthropicServiceTierWrapper,
|
||||
resolveAnthropicBetas,
|
||||
resolveAnthropicFastMode,
|
||||
resolveAnthropicServiceTier,
|
||||
} from "./stream-wrappers.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 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({
|
||||
providerId: PROVIDER_ID,
|
||||
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<ProviderAuthResult> {
|
||||
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<ProviderAuthContext["config"] | null> {
|
||||
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",
|
||||
});
|
||||
}
|
||||
|
||||
async function runAnthropicCliMigration(ctx: ProviderAuthContext): Promise<ProviderAuthResult> {
|
||||
if (!hasClaudeCliAuth()) {
|
||||
throw new Error(
|
||||
[
|
||||
"Claude CLI is not authenticated on this host.",
|
||||
`Run ${formatCliCommand("claude auth login")} first, then re-run this setup.`,
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
return buildAnthropicCliMigrationResult(ctx.config);
|
||||
}
|
||||
|
||||
async function runAnthropicCliMigrationNonInteractive(ctx: {
|
||||
config: ProviderAuthContext["config"];
|
||||
runtime: ProviderAuthContext["runtime"];
|
||||
}): Promise<ProviderAuthContext["config"] | null> {
|
||||
if (!hasClaudeCliAuth()) {
|
||||
ctx.runtime.error(
|
||||
[
|
||||
'Auth choice "anthropic-cli" requires Claude CLI auth on this host.',
|
||||
`Run ${formatCliCommand("claude auth login")} first.`,
|
||||
].join("\n"),
|
||||
);
|
||||
ctx.runtime.exit(1);
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = buildAnthropicCliMigrationResult(ctx.config);
|
||||
const currentDefaults = ctx.config.agents?.defaults;
|
||||
const currentModel = currentDefaults?.model;
|
||||
const currentFallbacks =
|
||||
currentModel && typeof currentModel === "object" && "fallbacks" in currentModel
|
||||
? currentModel.fallbacks
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
...ctx.config,
|
||||
...result.configPatch,
|
||||
agents: {
|
||||
...ctx.config.agents,
|
||||
...result.configPatch?.agents,
|
||||
defaults: {
|
||||
...currentDefaults,
|
||||
...result.configPatch?.agents?.defaults,
|
||||
model: {
|
||||
...(Array.isArray(currentFallbacks) ? { fallbacks: currentFallbacks } : {}),
|
||||
primary: result.defaultModel,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
||||
|
||||
export default definePluginEntry({
|
||||
id: PROVIDER_ID,
|
||||
id: "anthropic",
|
||||
name: "Anthropic Provider",
|
||||
description: "Bundled Anthropic provider plugin",
|
||||
register(api) {
|
||||
api.registerCliBackend(buildAnthropicCliBackend());
|
||||
api.registerProvider({
|
||||
id: PROVIDER_ID,
|
||||
label: "Anthropic",
|
||||
docsPath: "/providers/models",
|
||||
envVars: ["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"],
|
||||
deprecatedProfileIds: [CLAUDE_CLI_PROFILE_ID],
|
||||
oauthProfileIdRepairs: [
|
||||
{
|
||||
legacyProfileId: "anthropic:default",
|
||||
promptLabel: "Anthropic",
|
||||
},
|
||||
],
|
||||
auth: [
|
||||
{
|
||||
id: "cli",
|
||||
label: "Claude CLI",
|
||||
hint: "Reuse a local Claude CLI login and switch model selection to claude-cli/*",
|
||||
kind: "custom",
|
||||
wizard: {
|
||||
choiceId: "anthropic-cli",
|
||||
choiceLabel: "Anthropic Claude CLI",
|
||||
choiceHint: "Reuse a local Claude CLI login on this host",
|
||||
assistantPriority: -20,
|
||||
groupId: "anthropic",
|
||||
groupLabel: "Anthropic",
|
||||
groupHint: "Claude CLI + setup-token + API key",
|
||||
modelAllowlist: {
|
||||
allowedKeys: [...ANTHROPIC_OAUTH_ALLOWLIST].map((model) =>
|
||||
model.replace(/^anthropic\//, "claude-cli/"),
|
||||
),
|
||||
initialSelections: ["claude-cli/claude-sonnet-4-6"],
|
||||
message: "Claude CLI models",
|
||||
},
|
||||
},
|
||||
run: async (ctx: ProviderAuthContext) => await runAnthropicCliMigration(ctx),
|
||||
runNonInteractive: async (ctx) =>
|
||||
await runAnthropicCliMigrationNonInteractive({
|
||||
config: ctx.config,
|
||||
runtime: ctx.runtime,
|
||||
}),
|
||||
},
|
||||
{
|
||||
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",
|
||||
assistantVisibility: "manual-only",
|
||||
groupId: "anthropic",
|
||||
groupLabel: "Anthropic",
|
||||
groupHint: "Claude CLI + 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: "Claude CLI + setup-token + API key",
|
||||
},
|
||||
}),
|
||||
],
|
||||
resolveDynamicModel: (ctx) => resolveAnthropicForwardCompatModel(ctx),
|
||||
buildReplayPolicy: (ctx) => buildAnthropicReplayPolicy(ctx),
|
||||
isModernModelRef: ({ modelId }) => matchesAnthropicModernModel(modelId),
|
||||
resolveReasoningOutputMode: () => "native",
|
||||
wrapStreamFn: (ctx) => {
|
||||
const anthropicBetas = resolveAnthropicBetas(ctx.extraParams, ctx.modelId);
|
||||
const serviceTier = resolveAnthropicServiceTier(ctx.extraParams);
|
||||
const fastMode = resolveAnthropicFastMode(ctx.extraParams);
|
||||
return composeProviderStreamWrappers(
|
||||
ctx.streamFn,
|
||||
anthropicBetas?.length
|
||||
? (streamFn) => createAnthropicBetaHeadersWrapper(streamFn, anthropicBetas)
|
||||
: undefined,
|
||||
serviceTier
|
||||
? (streamFn) => createAnthropicServiceTierWrapper(streamFn, serviceTier)
|
||||
: undefined,
|
||||
fastMode !== undefined
|
||||
? (streamFn) => createAnthropicFastModeWrapper(streamFn, fastMode)
|
||||
: undefined,
|
||||
);
|
||||
},
|
||||
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);
|
||||
async register(api) {
|
||||
const { registerAnthropicPlugin } = await import("./register.runtime.js");
|
||||
await registerAnthropicPlugin(api);
|
||||
},
|
||||
});
|
||||
|
||||
488
extensions/anthropic/register.runtime.ts
Normal file
488
extensions/anthropic/register.runtime.ts
Normal file
@@ -0,0 +1,488 @@
|
||||
import { formatCliCommand } from "openclaw/plugin-sdk/cli-runtime";
|
||||
import { parseDurationMs } from "openclaw/plugin-sdk/cli-runtime";
|
||||
import type {
|
||||
OpenClawPluginApi,
|
||||
ProviderAuthContext,
|
||||
ProviderResolveDynamicModelContext,
|
||||
ProviderRuntimeModel,
|
||||
} from "openclaw/plugin-sdk/plugin-entry";
|
||||
import {
|
||||
CLAUDE_CLI_PROFILE_ID,
|
||||
applyAuthProfileConfig,
|
||||
buildTokenProfileId,
|
||||
ensureApiKeyFromOptionEnvOrPrompt,
|
||||
listProfilesForProvider,
|
||||
normalizeApiKeyInput,
|
||||
normalizeSecretInput,
|
||||
normalizeSecretInputModeInput,
|
||||
promptSecretRefForSetup,
|
||||
resolveSecretInputModeForEnvSelection,
|
||||
suggestOAuthProfileIdForLegacyDefault,
|
||||
type AuthProfileStore,
|
||||
type ProviderAuthResult,
|
||||
upsertAuthProfile,
|
||||
validateAnthropicSetupToken,
|
||||
validateApiKeyInput,
|
||||
} from "openclaw/plugin-sdk/provider-auth";
|
||||
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key";
|
||||
import { cloneFirstTemplateModel } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { composeProviderStreamWrappers } from "openclaw/plugin-sdk/provider-stream";
|
||||
import { fetchClaudeUsage } from "openclaw/plugin-sdk/provider-usage";
|
||||
import { buildAnthropicCliBackend } from "./cli-backend.js";
|
||||
import { buildAnthropicCliMigrationResult, hasClaudeCliAuth } from "./cli-migration.js";
|
||||
import { anthropicMediaUnderstandingProvider } from "./media-understanding-provider.js";
|
||||
import { buildAnthropicReplayPolicy } from "./replay-policy.js";
|
||||
import {
|
||||
createAnthropicBetaHeadersWrapper,
|
||||
createAnthropicFastModeWrapper,
|
||||
createAnthropicServiceTierWrapper,
|
||||
resolveAnthropicBetas,
|
||||
resolveAnthropicFastMode,
|
||||
resolveAnthropicServiceTier,
|
||||
} from "./stream-wrappers.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 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({
|
||||
providerId: PROVIDER_ID,
|
||||
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<ProviderAuthResult> {
|
||||
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<ProviderAuthContext["config"] | null> {
|
||||
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",
|
||||
});
|
||||
}
|
||||
|
||||
async function runAnthropicCliMigration(ctx: ProviderAuthContext): Promise<ProviderAuthResult> {
|
||||
if (!hasClaudeCliAuth()) {
|
||||
throw new Error(
|
||||
[
|
||||
"Claude CLI is not authenticated on this host.",
|
||||
`Run ${formatCliCommand("claude auth login")} first, then re-run this setup.`,
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
return buildAnthropicCliMigrationResult(ctx.config);
|
||||
}
|
||||
|
||||
async function runAnthropicCliMigrationNonInteractive(ctx: {
|
||||
config: ProviderAuthContext["config"];
|
||||
runtime: ProviderAuthContext["runtime"];
|
||||
}): Promise<ProviderAuthContext["config"] | null> {
|
||||
if (!hasClaudeCliAuth()) {
|
||||
ctx.runtime.error(
|
||||
[
|
||||
'Auth choice "anthropic-cli" requires Claude CLI auth on this host.',
|
||||
`Run ${formatCliCommand("claude auth login")} first.`,
|
||||
].join("\n"),
|
||||
);
|
||||
ctx.runtime.exit(1);
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = buildAnthropicCliMigrationResult(ctx.config);
|
||||
const currentDefaults = ctx.config.agents?.defaults;
|
||||
const currentModel = currentDefaults?.model;
|
||||
const currentFallbacks =
|
||||
currentModel && typeof currentModel === "object" && "fallbacks" in currentModel
|
||||
? currentModel.fallbacks
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
...ctx.config,
|
||||
...result.configPatch,
|
||||
agents: {
|
||||
...ctx.config.agents,
|
||||
...result.configPatch?.agents,
|
||||
defaults: {
|
||||
...currentDefaults,
|
||||
...result.configPatch?.agents?.defaults,
|
||||
model: {
|
||||
...(Array.isArray(currentFallbacks) ? { fallbacks: currentFallbacks } : {}),
|
||||
primary: result.defaultModel,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function registerAnthropicPlugin(api: OpenClawPluginApi): Promise<void> {
|
||||
api.registerCliBackend(buildAnthropicCliBackend());
|
||||
api.registerProvider({
|
||||
id: PROVIDER_ID,
|
||||
label: "Anthropic",
|
||||
docsPath: "/providers/models",
|
||||
envVars: ["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"],
|
||||
deprecatedProfileIds: [CLAUDE_CLI_PROFILE_ID],
|
||||
oauthProfileIdRepairs: [
|
||||
{
|
||||
legacyProfileId: "anthropic:default",
|
||||
promptLabel: "Anthropic",
|
||||
},
|
||||
],
|
||||
auth: [
|
||||
{
|
||||
id: "cli",
|
||||
label: "Claude CLI",
|
||||
hint: "Reuse a local Claude CLI login and switch model selection to claude-cli/*",
|
||||
kind: "custom",
|
||||
wizard: {
|
||||
choiceId: "anthropic-cli",
|
||||
choiceLabel: "Anthropic Claude CLI",
|
||||
choiceHint: "Reuse a local Claude CLI login on this host",
|
||||
assistantPriority: -20,
|
||||
groupId: "anthropic",
|
||||
groupLabel: "Anthropic",
|
||||
groupHint: "Claude CLI + setup-token + API key",
|
||||
modelAllowlist: {
|
||||
allowedKeys: [...ANTHROPIC_OAUTH_ALLOWLIST].map((model) =>
|
||||
model.replace(/^anthropic\//, "claude-cli/"),
|
||||
),
|
||||
initialSelections: ["claude-cli/claude-sonnet-4-6"],
|
||||
message: "Claude CLI models",
|
||||
},
|
||||
},
|
||||
run: async (ctx: ProviderAuthContext) => await runAnthropicCliMigration(ctx),
|
||||
runNonInteractive: async (ctx) =>
|
||||
await runAnthropicCliMigrationNonInteractive({
|
||||
config: ctx.config,
|
||||
runtime: ctx.runtime,
|
||||
}),
|
||||
},
|
||||
{
|
||||
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",
|
||||
assistantVisibility: "manual-only",
|
||||
groupId: "anthropic",
|
||||
groupLabel: "Anthropic",
|
||||
groupHint: "Claude CLI + 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: "Claude CLI + setup-token + API key",
|
||||
},
|
||||
}),
|
||||
],
|
||||
resolveDynamicModel: (ctx) => resolveAnthropicForwardCompatModel(ctx),
|
||||
buildReplayPolicy: (ctx) => buildAnthropicReplayPolicy(ctx),
|
||||
isModernModelRef: ({ modelId }) => matchesAnthropicModernModel(modelId),
|
||||
resolveReasoningOutputMode: () => "native",
|
||||
wrapStreamFn: (ctx) => {
|
||||
const anthropicBetas = resolveAnthropicBetas(ctx.extraParams, ctx.modelId);
|
||||
const serviceTier = resolveAnthropicServiceTier(ctx.extraParams);
|
||||
const fastMode = resolveAnthropicFastMode(ctx.extraParams);
|
||||
return composeProviderStreamWrappers(
|
||||
ctx.streamFn,
|
||||
anthropicBetas?.length
|
||||
? (streamFn) => createAnthropicBetaHeadersWrapper(streamFn, anthropicBetas)
|
||||
: undefined,
|
||||
serviceTier
|
||||
? (streamFn) => createAnthropicServiceTierWrapper(streamFn, serviceTier)
|
||||
: undefined,
|
||||
fastMode !== undefined
|
||||
? (streamFn) => createAnthropicFastModeWrapper(streamFn, fastMode)
|
||||
: undefined,
|
||||
);
|
||||
},
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user