Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
878 lines
30 KiB
TypeScript
878 lines
30 KiB
TypeScript
/**
|
|
* Anthropic provider runtime registration. It owns API-key/setup-token/Claude
|
|
* CLI auth, dynamic model normalization, usage auth, media, and stream wrappers.
|
|
*/
|
|
import { formatCliCommand, parseDurationMs } from "openclaw/plugin-sdk/cli-runtime";
|
|
import { resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
|
|
import type {
|
|
OpenClawPluginApi,
|
|
ProviderAuthContext,
|
|
ProviderAuthMethodNonInteractiveContext,
|
|
ProviderResolveDynamicModelContext,
|
|
ProviderNormalizeResolvedModelContext,
|
|
ProviderResolveUsageAuthContext,
|
|
ProviderResolvedUsageAuth,
|
|
ProviderRuntimeModel,
|
|
} from "openclaw/plugin-sdk/plugin-entry";
|
|
import {
|
|
applyAuthProfileConfig,
|
|
type AuthProfileStore,
|
|
buildTokenProfileId,
|
|
createProviderApiKeyAuthMethod,
|
|
listProfilesForProvider,
|
|
type OpenClawConfig as ProviderAuthConfig,
|
|
type ProviderAuthResult,
|
|
suggestOAuthProfileIdForLegacyDefault,
|
|
upsertAuthProfileWithLock,
|
|
validateAnthropicSetupToken,
|
|
} from "openclaw/plugin-sdk/provider-auth";
|
|
import {
|
|
cloneFirstTemplateModel,
|
|
NATIVE_ANTHROPIC_REPLAY_HOOKS,
|
|
type ProviderPlugin,
|
|
resolveClaudeFable5ModelIdentity,
|
|
resolveClaudeModelIdentity,
|
|
resolveClaudeThinkingProfile,
|
|
supportsClaudeAdaptiveThinking,
|
|
supportsClaudeNativeMaxEffort,
|
|
supportsClaudeNativeXhighEffort,
|
|
} from "openclaw/plugin-sdk/provider-model-shared";
|
|
import { fetchClaudeUsage } from "openclaw/plugin-sdk/provider-usage";
|
|
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
import * as claudeCliAuth from "./cli-auth-seam.js";
|
|
import { buildAnthropicCliBackend } from "./cli-backend.js";
|
|
import { buildClaudeCliCatalogEntries } from "./cli-catalog.js";
|
|
import { buildAnthropicCliMigrationResult } from "./cli-migration.js";
|
|
import {
|
|
CLAUDE_CLI_BACKEND_ID,
|
|
CLAUDE_CLI_DEFAULT_ALLOWLIST_REFS,
|
|
CLAUDE_CLI_DEFAULT_MODEL_REF,
|
|
CLAUDE_CLI_OFF_THINKING_PROFILE,
|
|
} from "./cli-shared.js";
|
|
import {
|
|
applyAnthropicConfigDefaults,
|
|
normalizeAnthropicProviderConfigForProvider,
|
|
} from "./config-defaults.js";
|
|
import { anthropicMediaUnderstandingProvider } from "./media-understanding-provider.js";
|
|
import { wrapAnthropicProviderStream } from "./stream-wrappers.js";
|
|
|
|
const PROVIDER_ID = "anthropic";
|
|
type UpsertAuthProfileParams = Parameters<typeof upsertAuthProfileWithLock>[0];
|
|
const DEFAULT_ANTHROPIC_MODEL = "anthropic/claude-opus-4-8";
|
|
const ANTHROPIC_OPUS_48_MODEL_ID = "claude-opus-4-8";
|
|
const ANTHROPIC_OPUS_48_DOT_MODEL_ID = "claude-opus-4.8";
|
|
const ANTHROPIC_OPUS_47_MODEL_ID = "claude-opus-4-7";
|
|
const ANTHROPIC_OPUS_47_DOT_MODEL_ID = "claude-opus-4.7";
|
|
const ANTHROPIC_GA_1M_CONTEXT_TOKENS = 1_048_576;
|
|
const ANTHROPIC_FABLE_CONTEXT_TOKENS = 1_000_000;
|
|
const ANTHROPIC_MODERN_MAX_OUTPUT_TOKENS = 128_000;
|
|
const ANTHROPIC_OPUS_46_MODEL_ID = "claude-opus-4-6";
|
|
const ANTHROPIC_OPUS_46_DOT_MODEL_ID = "claude-opus-4.6";
|
|
const ANTHROPIC_OPUS_47_TEMPLATE_MODEL_IDS = [
|
|
ANTHROPIC_OPUS_46_MODEL_ID,
|
|
ANTHROPIC_OPUS_46_DOT_MODEL_ID,
|
|
] 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_SETUP_TOKEN_NOTE_LINES = [
|
|
"Anthropic setup-token auth is supported in OpenClaw.",
|
|
"OpenClaw prefers Claude CLI reuse when it is available on the host.",
|
|
"Anthropic staff told us this OpenClaw path is allowed again.",
|
|
`If you want a direct API billing path instead, use ${formatCliCommand("openclaw models auth login --provider anthropic --method api-key --set-default")} or ${formatCliCommand("openclaw models auth login --provider anthropic --method cli --set-default")}.`,
|
|
] as const;
|
|
|
|
const CLAUDE_CLI_CANONICAL_ALLOWLIST_REFS = CLAUDE_CLI_DEFAULT_ALLOWLIST_REFS.map((ref) =>
|
|
ref.startsWith(`${CLAUDE_CLI_BACKEND_ID}/`)
|
|
? `anthropic/${ref.slice(CLAUDE_CLI_BACKEND_ID.length + 1)}`
|
|
: ref,
|
|
);
|
|
|
|
async function upsertAuthProfileWithLockOrThrow(params: UpsertAuthProfileParams): Promise<void> {
|
|
const updated = await upsertAuthProfileWithLock(params);
|
|
if (!updated) {
|
|
throw new Error(
|
|
"Failed to update auth profile store; the auth store lock may be busy. Wait a moment and retry.",
|
|
);
|
|
}
|
|
}
|
|
const CLAUDE_CLI_CANONICAL_DEFAULT_MODEL_REF = CLAUDE_CLI_DEFAULT_MODEL_REF.startsWith(
|
|
`${CLAUDE_CLI_BACKEND_ID}/`,
|
|
)
|
|
? `anthropic/${CLAUDE_CLI_DEFAULT_MODEL_REF.slice(CLAUDE_CLI_BACKEND_ID.length + 1)}`
|
|
: CLAUDE_CLI_DEFAULT_MODEL_REF;
|
|
|
|
function normalizeAnthropicSetupTokenInput(value: string): string {
|
|
return value.replaceAll(/\s+/g, "").trim();
|
|
}
|
|
|
|
function resolveAnthropicSetupTokenProfileId(rawProfileId?: unknown): string {
|
|
if (typeof rawProfileId === "string") {
|
|
const trimmed = rawProfileId.trim();
|
|
if (trimmed.length > 0) {
|
|
if (trimmed.startsWith(`${PROVIDER_ID}:`)) {
|
|
return trimmed;
|
|
}
|
|
return buildTokenProfileId({ provider: PROVIDER_ID, name: trimmed });
|
|
}
|
|
}
|
|
return `${PROVIDER_ID}:default`;
|
|
}
|
|
|
|
function resolveAnthropicSetupTokenExpiry(rawExpiresIn?: unknown): number | undefined {
|
|
if (typeof rawExpiresIn !== "string" || rawExpiresIn.trim().length === 0) {
|
|
return undefined;
|
|
}
|
|
return resolveExpiresAtMsFromDurationMs(
|
|
parseDurationMs(rawExpiresIn.trim(), { defaultUnit: "d" }),
|
|
);
|
|
}
|
|
|
|
async function runAnthropicSetupTokenAuth(ctx: ProviderAuthContext): Promise<ProviderAuthResult> {
|
|
const providedToken =
|
|
typeof ctx.opts?.token === "string" && ctx.opts.token.trim().length > 0
|
|
? normalizeAnthropicSetupTokenInput(ctx.opts.token)
|
|
: undefined;
|
|
const token =
|
|
providedToken ??
|
|
normalizeAnthropicSetupTokenInput(
|
|
await ctx.prompter.text({
|
|
message: "Paste Anthropic setup-token",
|
|
validate: (value) => validateAnthropicSetupToken(normalizeAnthropicSetupTokenInput(value)),
|
|
}),
|
|
);
|
|
const tokenError = validateAnthropicSetupToken(token);
|
|
if (tokenError) {
|
|
throw new Error(tokenError);
|
|
}
|
|
|
|
const profileId = resolveAnthropicSetupTokenProfileId(ctx.opts?.tokenProfileId);
|
|
const expires = resolveAnthropicSetupTokenExpiry(ctx.opts?.tokenExpiresIn);
|
|
|
|
return {
|
|
profiles: [
|
|
{
|
|
profileId,
|
|
credential: {
|
|
type: "token",
|
|
provider: PROVIDER_ID,
|
|
token,
|
|
...(expires ? { expires } : {}),
|
|
},
|
|
},
|
|
],
|
|
defaultModel: DEFAULT_ANTHROPIC_MODEL,
|
|
notes: [...ANTHROPIC_SETUP_TOKEN_NOTE_LINES],
|
|
};
|
|
}
|
|
|
|
async function runAnthropicSetupTokenNonInteractive(
|
|
ctx: ProviderAuthMethodNonInteractiveContext,
|
|
): Promise<ProviderAuthConfig | null> {
|
|
const rawToken =
|
|
typeof ctx.opts.token === "string" ? normalizeAnthropicSetupTokenInput(ctx.opts.token) : "";
|
|
const tokenError = validateAnthropicSetupToken(rawToken);
|
|
if (tokenError) {
|
|
ctx.runtime.error(
|
|
["Anthropic setup-token auth requires --token with a valid setup-token.", tokenError].join(
|
|
"\n",
|
|
),
|
|
);
|
|
ctx.runtime.exit(1);
|
|
return null;
|
|
}
|
|
|
|
const profileId = resolveAnthropicSetupTokenProfileId(ctx.opts.tokenProfileId);
|
|
const expires = resolveAnthropicSetupTokenExpiry(ctx.opts.tokenExpiresIn);
|
|
await upsertAuthProfileWithLockOrThrow({
|
|
profileId,
|
|
credential: {
|
|
type: "token",
|
|
provider: PROVIDER_ID,
|
|
token: rawToken,
|
|
...(expires ? { expires } : {}),
|
|
},
|
|
agentDir: ctx.agentDir,
|
|
});
|
|
|
|
ctx.runtime.log(ANTHROPIC_SETUP_TOKEN_NOTE_LINES[0]);
|
|
ctx.runtime.log(ANTHROPIC_SETUP_TOKEN_NOTE_LINES[1]);
|
|
|
|
const withProfile = applyAuthProfileConfig(ctx.config, {
|
|
profileId,
|
|
provider: PROVIDER_ID,
|
|
mode: "token",
|
|
});
|
|
const existingModelConfig =
|
|
withProfile.agents?.defaults?.model && typeof withProfile.agents.defaults.model === "object"
|
|
? withProfile.agents.defaults.model
|
|
: {};
|
|
return {
|
|
...withProfile,
|
|
agents: {
|
|
...withProfile.agents,
|
|
defaults: {
|
|
...withProfile.agents?.defaults,
|
|
model: {
|
|
...existingModelConfig,
|
|
primary: DEFAULT_ANTHROPIC_MODEL,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
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 = normalizeLowercaseStringOrEmpty(trimmedModelId);
|
|
if (trimmedModelId !== lower) {
|
|
return undefined;
|
|
}
|
|
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,
|
|
patch:
|
|
normalizeLowercaseStringOrEmpty(params.ctx.provider) === CLAUDE_CLI_BACKEND_ID
|
|
? { provider: CLAUDE_CLI_BACKEND_ID }
|
|
: undefined,
|
|
});
|
|
}
|
|
|
|
function buildAnthropicForwardCompatModel(
|
|
ctx: ProviderResolveDynamicModelContext,
|
|
): ProviderRuntimeModel | undefined {
|
|
const trimmedModelId = ctx.modelId.trim();
|
|
const lower = normalizeLowercaseStringOrEmpty(trimmedModelId);
|
|
const normalizedProvider = normalizeLowercaseStringOrEmpty(ctx.provider);
|
|
if (trimmedModelId !== lower || !matchesAnthropicModernModel(lower)) {
|
|
return undefined;
|
|
}
|
|
if (isAnthropicFable5Model(lower) && normalizedProvider !== PROVIDER_ID) {
|
|
return undefined;
|
|
}
|
|
const provider =
|
|
normalizedProvider === CLAUDE_CLI_BACKEND_ID ? CLAUDE_CLI_BACKEND_ID : PROVIDER_ID;
|
|
return {
|
|
id: trimmedModelId,
|
|
name: trimmedModelId,
|
|
provider,
|
|
api: "anthropic-messages",
|
|
baseUrl: "https://api.anthropic.com",
|
|
reasoning: true,
|
|
input: ["text", "image"],
|
|
cost: isAnthropicFable5Model(trimmedModelId)
|
|
? { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 }
|
|
: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: resolveAnthropicFixedContextWindow(trimmedModelId) ?? 200_000,
|
|
maxTokens: isAnthropic128kOutputModel(trimmedModelId)
|
|
? ANTHROPIC_MODERN_MAX_OUTPUT_TOKENS
|
|
: 64_000,
|
|
...(supportsClaudeNativeXhighEffort({ id: trimmedModelId })
|
|
? { thinkingLevelMap: { xhigh: "xhigh", max: "max" } }
|
|
: supportsAnthropicNativeMaxEffort(trimmedModelId)
|
|
? { thinkingLevelMap: { max: "max" } }
|
|
: {}),
|
|
};
|
|
}
|
|
|
|
function resolveAnthropicForwardCompatModel(
|
|
ctx: ProviderResolveDynamicModelContext,
|
|
): ProviderRuntimeModel | undefined {
|
|
return (
|
|
resolveAnthropic46ForwardCompatModel({
|
|
ctx,
|
|
dashModelId: ANTHROPIC_OPUS_48_MODEL_ID,
|
|
dotModelId: ANTHROPIC_OPUS_48_DOT_MODEL_ID,
|
|
dashTemplateId: ANTHROPIC_OPUS_47_MODEL_ID,
|
|
dotTemplateId: ANTHROPIC_OPUS_47_DOT_MODEL_ID,
|
|
fallbackTemplateIds: ANTHROPIC_OPUS_47_TEMPLATE_MODEL_IDS,
|
|
}) ??
|
|
resolveAnthropic46ForwardCompatModel({
|
|
ctx,
|
|
dashModelId: ANTHROPIC_OPUS_47_MODEL_ID,
|
|
dotModelId: ANTHROPIC_OPUS_47_DOT_MODEL_ID,
|
|
dashTemplateId: ANTHROPIC_OPUS_46_MODEL_ID,
|
|
dotTemplateId: ANTHROPIC_OPUS_46_DOT_MODEL_ID,
|
|
fallbackTemplateIds: ANTHROPIC_OPUS_47_TEMPLATE_MODEL_IDS,
|
|
}) ??
|
|
resolveAnthropic46ForwardCompatModel({
|
|
ctx,
|
|
dashModelId: ANTHROPIC_OPUS_46_MODEL_ID,
|
|
dotModelId: ANTHROPIC_OPUS_46_DOT_MODEL_ID,
|
|
dashTemplateId: ANTHROPIC_OPUS_47_MODEL_ID,
|
|
dotTemplateId: ANTHROPIC_OPUS_46_MODEL_ID,
|
|
fallbackTemplateIds: ANTHROPIC_OPUS_47_TEMPLATE_MODEL_IDS,
|
|
}) ??
|
|
resolveAnthropic46ForwardCompatModel({
|
|
ctx,
|
|
dashModelId: ANTHROPIC_SONNET_46_MODEL_ID,
|
|
dotModelId: ANTHROPIC_SONNET_46_DOT_MODEL_ID,
|
|
dashTemplateId: ANTHROPIC_SONNET_46_MODEL_ID,
|
|
dotTemplateId: ANTHROPIC_SONNET_46_MODEL_ID,
|
|
fallbackTemplateIds: [ANTHROPIC_SONNET_46_MODEL_ID, ANTHROPIC_SONNET_46_DOT_MODEL_ID],
|
|
}) ??
|
|
buildAnthropicForwardCompatModel(ctx)
|
|
);
|
|
}
|
|
|
|
function isAnthropicGa1MModel(modelId: string): boolean {
|
|
return supportsClaudeAdaptiveThinking({ id: modelId });
|
|
}
|
|
|
|
function isAnthropicFable5Model(modelId: string): boolean {
|
|
return resolveClaudeFable5ModelIdentity({ id: modelId }) !== undefined;
|
|
}
|
|
|
|
function resolveAnthropicFixedContextWindow(modelId: string): number | undefined {
|
|
if (isAnthropicFable5Model(modelId)) {
|
|
return ANTHROPIC_FABLE_CONTEXT_TOKENS;
|
|
}
|
|
return isAnthropicGa1MModel(modelId) ? ANTHROPIC_GA_1M_CONTEXT_TOKENS : undefined;
|
|
}
|
|
|
|
function isAnthropic128kOutputModel(modelId: string): boolean {
|
|
if (isAnthropicFable5Model(modelId)) {
|
|
return true;
|
|
}
|
|
return /^claude-opus-4-8(?=$|[^a-z0-9])/.test(resolveClaudeModelIdentity({ id: modelId }));
|
|
}
|
|
|
|
function isAnthropicOpus47OrNewerModel(modelId: string): boolean {
|
|
return supportsClaudeNativeXhighEffort({ id: modelId }) && !isAnthropicFable5Model(modelId);
|
|
}
|
|
|
|
function isAnthropicMythosPreviewModel(modelId: string): boolean {
|
|
return /(?:^|-)claude-mythos-preview(?=$|[^a-z0-9])/.test(
|
|
resolveClaudeModelIdentity({ id: modelId }),
|
|
);
|
|
}
|
|
|
|
function supportsAnthropicNativeMaxEffort(modelId: string): boolean {
|
|
return supportsClaudeNativeMaxEffort({ id: modelId }) || isAnthropicMythosPreviewModel(modelId);
|
|
}
|
|
|
|
function hasConfiguredModelContextOverride(
|
|
config: ProviderNormalizeResolvedModelContext["config"],
|
|
provider: string,
|
|
modelId: string,
|
|
): boolean {
|
|
const providers = config?.models?.providers;
|
|
if (!providers || typeof providers !== "object") {
|
|
return false;
|
|
}
|
|
const normalizedProvider = normalizeLowercaseStringOrEmpty(provider);
|
|
const normalizedModelId = normalizeLowercaseStringOrEmpty(modelId);
|
|
for (const [providerId, providerConfig] of Object.entries(providers)) {
|
|
if (normalizeLowercaseStringOrEmpty(providerId) !== normalizedProvider) {
|
|
continue;
|
|
}
|
|
if (!Array.isArray(providerConfig?.models)) {
|
|
continue;
|
|
}
|
|
for (const model of providerConfig.models) {
|
|
if (
|
|
normalizeLowercaseStringOrEmpty(typeof model?.id === "string" ? model.id : "") !==
|
|
normalizedModelId
|
|
) {
|
|
continue;
|
|
}
|
|
if (
|
|
(typeof model?.contextTokens === "number" && model.contextTokens > 0) ||
|
|
(typeof model?.contextWindow === "number" && model.contextWindow > 0)
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function applyAnthropicFixedContextWindow(params: {
|
|
config?: ProviderNormalizeResolvedModelContext["config"];
|
|
provider: string;
|
|
modelId: string;
|
|
contractModelId: string;
|
|
model: ProviderRuntimeModel;
|
|
}): ProviderRuntimeModel | undefined {
|
|
const fixedContextWindow = resolveAnthropicFixedContextWindow(params.contractModelId);
|
|
if (fixedContextWindow === undefined) {
|
|
return undefined;
|
|
}
|
|
if (hasConfiguredModelContextOverride(params.config, params.provider, params.modelId)) {
|
|
return undefined;
|
|
}
|
|
const exactContextWindow = isAnthropicFable5Model(params.contractModelId);
|
|
const nextContextWindow = exactContextWindow
|
|
? fixedContextWindow
|
|
: Math.max(params.model.contextWindow ?? 0, fixedContextWindow);
|
|
const nextContextTokens = exactContextWindow
|
|
? fixedContextWindow
|
|
: typeof params.model.contextTokens === "number"
|
|
? Math.max(params.model.contextTokens, fixedContextWindow)
|
|
: fixedContextWindow;
|
|
if (
|
|
nextContextWindow === params.model.contextWindow &&
|
|
nextContextTokens === params.model.contextTokens
|
|
) {
|
|
return undefined;
|
|
}
|
|
return {
|
|
...params.model,
|
|
contextWindow: nextContextWindow,
|
|
contextTokens: nextContextTokens,
|
|
};
|
|
}
|
|
|
|
function applyAnthropicModernMaxTokens(params: {
|
|
modelId: string;
|
|
model: ProviderRuntimeModel;
|
|
}): ProviderRuntimeModel | undefined {
|
|
if (!isAnthropic128kOutputModel(params.modelId)) {
|
|
return undefined;
|
|
}
|
|
if ((params.model.maxTokens ?? 0) >= ANTHROPIC_MODERN_MAX_OUTPUT_TOKENS) {
|
|
return undefined;
|
|
}
|
|
return {
|
|
...params.model,
|
|
maxTokens: ANTHROPIC_MODERN_MAX_OUTPUT_TOKENS,
|
|
};
|
|
}
|
|
|
|
function applyAnthropicThinkingLevelMap(params: {
|
|
modelId: string;
|
|
model: ProviderRuntimeModel;
|
|
}): ProviderRuntimeModel | undefined {
|
|
const fable5 = isAnthropicFable5Model(params.modelId);
|
|
const nativeXhigh = fable5 || isAnthropicOpus47OrNewerModel(params.modelId);
|
|
if (!supportsAnthropicNativeMaxEffort(params.modelId)) {
|
|
return undefined;
|
|
}
|
|
const current = params.model.thinkingLevelMap;
|
|
const nativeDefaults = isAnthropicMythosPreviewModel(params.modelId)
|
|
? { max: "max" as const }
|
|
: {
|
|
...(fable5 ? { off: "low" as const, minimal: "low" as const } : {}),
|
|
xhigh: nativeXhigh ? ("xhigh" as const) : null,
|
|
max: "max" as const,
|
|
};
|
|
const currentEfforts = current as Record<string, string | null | undefined> | undefined;
|
|
if (Object.keys(nativeDefaults).every((level) => currentEfforts?.[level] !== undefined)) {
|
|
return undefined;
|
|
}
|
|
return {
|
|
...params.model,
|
|
thinkingLevelMap: {
|
|
...nativeDefaults,
|
|
...current,
|
|
},
|
|
};
|
|
}
|
|
|
|
function matchesAnthropicModernModel(modelId: string): boolean {
|
|
return supportsClaudeAdaptiveThinking({ id: modelId }) || isAnthropicMythosPreviewModel(modelId);
|
|
}
|
|
|
|
function hasImageInput(input: unknown): boolean {
|
|
return Array.isArray(input) && input.includes("image");
|
|
}
|
|
|
|
function supportsAnthropicImageInput(modelId: string, modelName?: string): boolean {
|
|
return [modelId, modelName]
|
|
.filter((value): value is string => typeof value === "string")
|
|
.some((candidate) => matchesAnthropicModernModel(candidate));
|
|
}
|
|
|
|
function resolveAnthropicImageMediaInput(modelId: string, modelName?: string) {
|
|
if (!supportsAnthropicImageInput(modelId, modelName)) {
|
|
return undefined;
|
|
}
|
|
const refs = [modelId, modelName].filter((value): value is string => typeof value === "string");
|
|
const largeImageModel = refs.some(
|
|
(ref) => isAnthropicFable5Model(ref) || isAnthropicOpus47OrNewerModel(ref),
|
|
);
|
|
return {
|
|
image: {
|
|
maxSidePx: largeImageModel ? 2576 : 1568,
|
|
preferredSidePx: largeImageModel ? 2576 : 1568,
|
|
tokenMode: "provider" as const,
|
|
},
|
|
};
|
|
}
|
|
|
|
function applyAnthropicImageInputCapability(params: {
|
|
modelId: string;
|
|
model: ProviderRuntimeModel;
|
|
}): ProviderRuntimeModel | undefined {
|
|
if (hasImageInput(params.model.input)) {
|
|
return undefined;
|
|
}
|
|
if (!supportsAnthropicImageInput(params.modelId, params.model.name)) {
|
|
return undefined;
|
|
}
|
|
return {
|
|
...params.model,
|
|
input: ["text", "image"],
|
|
};
|
|
}
|
|
|
|
function normalizeAnthropicResolvedModel(
|
|
ctx: ProviderNormalizeResolvedModelContext,
|
|
): ProviderRuntimeModel | undefined {
|
|
const contractModelId = resolveClaudeModelIdentity({
|
|
id: ctx.modelId,
|
|
params: ctx.model.params,
|
|
});
|
|
if (
|
|
isAnthropicFable5Model(contractModelId) &&
|
|
normalizeLowercaseStringOrEmpty(ctx.provider) !== PROVIDER_ID
|
|
) {
|
|
return undefined;
|
|
}
|
|
const contractModel =
|
|
isAnthropicFable5Model(contractModelId) && !ctx.model.reasoning
|
|
? { ...ctx.model, reasoning: true }
|
|
: ctx.model;
|
|
const imageCapableModel =
|
|
applyAnthropicImageInputCapability({
|
|
modelId: contractModelId,
|
|
model: contractModel,
|
|
}) ?? contractModel;
|
|
const mediaInput = resolveAnthropicImageMediaInput(contractModelId, imageCapableModel.name);
|
|
const mediaInputModel = mediaInput
|
|
? {
|
|
...imageCapableModel,
|
|
mediaInput: {
|
|
...mediaInput,
|
|
...imageCapableModel.mediaInput,
|
|
image: {
|
|
...mediaInput.image,
|
|
...imageCapableModel.mediaInput?.image,
|
|
},
|
|
},
|
|
}
|
|
: imageCapableModel;
|
|
const outputModel =
|
|
applyAnthropicModernMaxTokens({
|
|
modelId: contractModelId,
|
|
model: mediaInputModel,
|
|
}) ?? mediaInputModel;
|
|
const thinkingLevelModel =
|
|
applyAnthropicThinkingLevelMap({
|
|
modelId: contractModelId,
|
|
model: outputModel,
|
|
}) ?? outputModel;
|
|
const contextWindowModel =
|
|
applyAnthropicFixedContextWindow({
|
|
config: ctx.config,
|
|
provider: ctx.provider,
|
|
modelId: ctx.modelId,
|
|
contractModelId,
|
|
model: thinkingLevelModel,
|
|
}) ?? thinkingLevelModel;
|
|
return contextWindowModel === ctx.model ? undefined : contextWindowModel;
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
function resolveClaudeCliSyntheticAuth() {
|
|
const credential = claudeCliAuth.readClaudeCliCredentialsForRuntime();
|
|
if (!credential) {
|
|
return undefined;
|
|
}
|
|
return credential.type === "oauth"
|
|
? {
|
|
apiKey: credential.access,
|
|
source: "Claude CLI native auth",
|
|
mode: "oauth" as const,
|
|
expiresAt: credential.expires,
|
|
}
|
|
: {
|
|
apiKey: credential.token,
|
|
source: "Claude CLI native auth",
|
|
mode: "token" as const,
|
|
expiresAt: credential.expires,
|
|
};
|
|
}
|
|
|
|
async function runAnthropicCliMigration(ctx: ProviderAuthContext): Promise<ProviderAuthResult> {
|
|
const credential = claudeCliAuth.readClaudeCliCredentialsForSetup();
|
|
if (!credential) {
|
|
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, credential);
|
|
}
|
|
|
|
async function runAnthropicCliMigrationNonInteractive(ctx: {
|
|
config: ProviderAuthContext["config"];
|
|
runtime: ProviderAuthContext["runtime"];
|
|
agentDir?: string;
|
|
}): Promise<ProviderAuthContext["config"] | null> {
|
|
const credential = claudeCliAuth.readClaudeCliCredentialsForSetupNonInteractive();
|
|
if (!credential) {
|
|
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, credential);
|
|
const currentDefaults = ctx.config.agents?.defaults;
|
|
const currentModel = currentDefaults?.model;
|
|
const currentFallbacks =
|
|
currentModel && typeof currentModel === "object" && "fallbacks" in currentModel
|
|
? currentModel.fallbacks
|
|
: undefined;
|
|
const migratedModel = result.configPatch?.agents?.defaults?.model;
|
|
const migratedFallbacks =
|
|
migratedModel && typeof migratedModel === "object" && "fallbacks" in migratedModel
|
|
? migratedModel.fallbacks
|
|
: undefined;
|
|
const nextFallbacks = Array.isArray(migratedFallbacks) ? migratedFallbacks : currentFallbacks;
|
|
|
|
return {
|
|
...ctx.config,
|
|
...result.configPatch,
|
|
agents: {
|
|
...ctx.config.agents,
|
|
...result.configPatch?.agents,
|
|
defaults: {
|
|
...currentDefaults,
|
|
...result.configPatch?.agents?.defaults,
|
|
model: {
|
|
...(Array.isArray(nextFallbacks) ? { fallbacks: nextFallbacks } : {}),
|
|
primary: result.defaultModel,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
async function resolveAnthropicUsageAuth(
|
|
ctx: ProviderResolveUsageAuthContext,
|
|
): Promise<ProviderResolvedUsageAuth> {
|
|
const oauthToken = await ctx.resolveOAuthToken();
|
|
if (oauthToken) {
|
|
return oauthToken;
|
|
}
|
|
|
|
const apiKey = ctx.resolveApiKeyFromConfigAndStore();
|
|
if (apiKey && validateAnthropicSetupToken(apiKey) === undefined) {
|
|
return { token: apiKey };
|
|
}
|
|
|
|
return { handled: true };
|
|
}
|
|
|
|
/** Build the full Anthropic provider descriptor used by runtime registration. */
|
|
export function buildAnthropicProvider(): ProviderPlugin {
|
|
const providerId = "anthropic";
|
|
const defaultAnthropicModel = DEFAULT_ANTHROPIC_MODEL;
|
|
return {
|
|
id: providerId,
|
|
label: "Anthropic",
|
|
docsPath: "/providers/models",
|
|
hookAliases: [CLAUDE_CLI_BACKEND_ID],
|
|
envVars: ["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"],
|
|
oauthProfileIdRepairs: [
|
|
{
|
|
legacyProfileId: "anthropic:default",
|
|
promptLabel: "Anthropic",
|
|
},
|
|
],
|
|
auth: [
|
|
{
|
|
id: "cli",
|
|
label: "Claude CLI",
|
|
hint: "Reuse a local Claude CLI login and run Anthropic models through the Claude CLI runtime",
|
|
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: [...CLAUDE_CLI_CANONICAL_ALLOWLIST_REFS],
|
|
initialSelections: [CLAUDE_CLI_CANONICAL_DEFAULT_MODEL_REF],
|
|
message: "Claude CLI models",
|
|
},
|
|
},
|
|
run: async (ctx: ProviderAuthContext) => await runAnthropicCliMigration(ctx),
|
|
runNonInteractive: async (ctx) =>
|
|
await runAnthropicCliMigrationNonInteractive({
|
|
config: ctx.config,
|
|
runtime: ctx.runtime,
|
|
agentDir: ctx.agentDir,
|
|
}),
|
|
},
|
|
{
|
|
id: "setup-token",
|
|
label: "Anthropic setup-token",
|
|
hint: "Manual bearer token path",
|
|
kind: "token",
|
|
wizard: {
|
|
choiceId: "setup-token",
|
|
choiceLabel: "Anthropic setup-token",
|
|
choiceHint: "Manual token path",
|
|
assistantPriority: 40,
|
|
groupId: "anthropic",
|
|
groupLabel: "Anthropic",
|
|
groupHint: "Claude CLI + API key + token",
|
|
},
|
|
run: async (ctx: ProviderAuthContext) => await runAnthropicSetupTokenAuth(ctx),
|
|
runNonInteractive: async (ctx: ProviderAuthMethodNonInteractiveContext) =>
|
|
await runAnthropicSetupTokenNonInteractive(ctx),
|
|
},
|
|
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: ({ provider, providerConfig }) =>
|
|
normalizeAnthropicProviderConfigForProvider({ provider, providerConfig }),
|
|
applyConfigDefaults: ({ config, env }) => applyAnthropicConfigDefaults({ config, env }),
|
|
resolveDynamicModel: (ctx) => {
|
|
const model = resolveAnthropicForwardCompatModel(ctx);
|
|
if (!model) {
|
|
return undefined;
|
|
}
|
|
return (
|
|
normalizeAnthropicResolvedModel({
|
|
config: ctx.config,
|
|
provider: ctx.provider,
|
|
modelId: ctx.modelId,
|
|
model,
|
|
}) ?? model
|
|
);
|
|
},
|
|
normalizeResolvedModel: (ctx) => normalizeAnthropicResolvedModel(ctx),
|
|
resolveSyntheticAuth: ({ provider }) =>
|
|
normalizeLowercaseStringOrEmpty(provider) === CLAUDE_CLI_BACKEND_ID
|
|
? resolveClaudeCliSyntheticAuth()
|
|
: undefined,
|
|
// Publish Claude CLI rows through the provider catalog hook.
|
|
augmentModelCatalog: () => buildClaudeCliCatalogEntries(),
|
|
...NATIVE_ANTHROPIC_REPLAY_HOOKS,
|
|
isModernModelRef: ({ provider, modelId }) =>
|
|
matchesAnthropicModernModel(modelId) &&
|
|
(!isAnthropicFable5Model(modelId) ||
|
|
normalizeLowercaseStringOrEmpty(provider) === PROVIDER_ID),
|
|
resolveReasoningOutputMode: () => "native",
|
|
resolveThinkingProfile: ({ provider, modelId, params }) => {
|
|
const contractModelId = resolveClaudeModelIdentity({ id: modelId, params });
|
|
return isAnthropicFable5Model(contractModelId) &&
|
|
normalizeLowercaseStringOrEmpty(provider) !== PROVIDER_ID
|
|
? CLAUDE_CLI_OFF_THINKING_PROFILE
|
|
: resolveClaudeThinkingProfile(contractModelId, undefined, {
|
|
includeNativeMax: [PROVIDER_ID, CLAUDE_CLI_BACKEND_ID].includes(
|
|
normalizeLowercaseStringOrEmpty(provider),
|
|
),
|
|
});
|
|
},
|
|
wrapStreamFn: wrapAnthropicProviderStream,
|
|
resolveUsageAuth: resolveAnthropicUsageAuth,
|
|
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,
|
|
}),
|
|
};
|
|
}
|
|
|
|
/** Register Anthropic provider, Claude CLI backend, and media understanding provider. */
|
|
export function registerAnthropicPlugin(api: OpenClawPluginApi): void {
|
|
api.registerCliBackend(buildAnthropicCliBackend());
|
|
api.registerProvider(buildAnthropicProvider());
|
|
api.registerMediaUnderstandingProvider(anthropicMediaUnderstandingProvider);
|
|
}
|