mirror of https://github.com/openclaw/openclaw.git
307 lines
11 KiB
TypeScript
307 lines
11 KiB
TypeScript
import { formatCliCommand } from "openclaw/plugin-sdk/cli-runtime";
|
|
import type {
|
|
OpenClawPluginApi,
|
|
ProviderAuthContext,
|
|
ProviderResolveDynamicModelContext,
|
|
ProviderRuntimeModel,
|
|
} from "openclaw/plugin-sdk/plugin-entry";
|
|
import {
|
|
applyAuthProfileConfig,
|
|
createProviderApiKeyAuthMethod,
|
|
ensureApiKeyFromOptionEnvOrPrompt,
|
|
listProfilesForProvider,
|
|
normalizeApiKeyInput,
|
|
suggestOAuthProfileIdForLegacyDefault,
|
|
type AuthProfileStore,
|
|
type ProviderAuthResult,
|
|
validateApiKeyInput,
|
|
} from "openclaw/plugin-sdk/provider-auth";
|
|
import { cloneFirstTemplateModel } from "openclaw/plugin-sdk/provider-model-shared";
|
|
import { fetchClaudeUsage } from "openclaw/plugin-sdk/provider-usage";
|
|
import { buildAnthropicCliBackend } from "./cli-backend.js";
|
|
import { buildAnthropicCliMigrationResult, hasClaudeCliAuth } from "./cli-migration.js";
|
|
import {
|
|
applyAnthropicConfigDefaults,
|
|
normalizeAnthropicProviderConfig,
|
|
} from "./config-defaults.js";
|
|
import { anthropicMediaUnderstandingProvider } from "./media-understanding-provider.js";
|
|
import { buildAnthropicReplayPolicy } from "./replay-policy.js";
|
|
import { wrapAnthropicProviderStream } 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 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 function registerAnthropicPlugin(api: OpenClawPluginApi): void {
|
|
const claudeCliProfileId = "anthropic:claude-cli";
|
|
const providerId = "anthropic";
|
|
const defaultAnthropicModel = "anthropic/claude-sonnet-4-6";
|
|
const anthropicOauthAllowlist = [
|
|
"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;
|
|
api.registerCliBackend(buildAnthropicCliBackend());
|
|
api.registerProvider({
|
|
id: providerId,
|
|
label: "Anthropic",
|
|
docsPath: "/providers/models",
|
|
envVars: ["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"],
|
|
deprecatedProfileIds: [claudeCliProfileId],
|
|
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 + API key",
|
|
modelAllowlist: {
|
|
allowedKeys: [...anthropicOauthAllowlist].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,
|
|
}),
|
|
},
|
|
createProviderApiKeyAuthMethod({
|
|
providerId,
|
|
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: defaultAnthropicModel,
|
|
expectedProviders: ["anthropic"],
|
|
wizard: {
|
|
choiceId: "apiKey",
|
|
choiceLabel: "Anthropic API key",
|
|
groupId: "anthropic",
|
|
groupLabel: "Anthropic",
|
|
groupHint: "Claude CLI + API key",
|
|
},
|
|
}),
|
|
],
|
|
normalizeConfig: ({ providerConfig }) => normalizeAnthropicProviderConfig(providerConfig),
|
|
applyConfigDefaults: ({ config, env }) => applyAnthropicConfigDefaults({ config, env }),
|
|
resolveDynamicModel: (ctx) => resolveAnthropicForwardCompatModel(ctx),
|
|
buildReplayPolicy: buildAnthropicReplayPolicy,
|
|
isModernModelRef: ({ modelId }) => matchesAnthropicModernModel(modelId),
|
|
resolveReasoningOutputMode: () => "native",
|
|
wrapStreamFn: wrapAnthropicProviderStream,
|
|
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);
|
|
}
|