Files
openclaw/src/plugins/provider-hook-runtime.ts
2026-04-28 02:38:31 +01:00

356 lines
12 KiB
TypeScript

import { normalizeProviderId } from "../agents/provider-id.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import { normalizePluginIdScope, serializePluginIdScope } from "./plugin-scope.js";
import { resolveProviderConfigApiOwnerHint } from "./provider-config-owner.js";
import { resolveOwningPluginIdsForProvider } from "./providers.js";
import { isPluginProvidersLoadInFlight, resolvePluginProviders } from "./providers.runtime.js";
import { resolvePluginCacheInputs } from "./roots.js";
import { getActivePluginRegistryWorkspaceDirFromState } from "./runtime-state.js";
import type {
ProviderPlugin,
ProviderExtraParamsForTransportContext,
ProviderPrepareExtraParamsContext,
ProviderResolveAuthProfileIdContext,
ProviderFollowupFallbackRouteContext,
ProviderFollowupFallbackRouteResult,
ProviderWrapStreamFnContext,
} from "./types.js";
function matchesProviderId(provider: ProviderPlugin, providerId: string): boolean {
const normalized = normalizeProviderId(providerId);
if (!normalized) {
return false;
}
if (normalizeProviderId(provider.id) === normalized) {
return true;
}
return [...(provider.aliases ?? []), ...(provider.hookAliases ?? [])].some(
(alias) => normalizeProviderId(alias) === normalized,
);
}
function matchesProviderLiteralId(provider: ProviderPlugin, providerId: string): boolean {
const normalized = normalizeLowercaseStringOrEmpty(providerId);
return !!normalized && normalizeLowercaseStringOrEmpty(provider.id) === normalized;
}
let cachedHookProviders = new WeakMap<NodeJS.ProcessEnv, Map<string, ProviderPlugin[]>>();
function resolveHookProviderCacheBucket(env: NodeJS.ProcessEnv) {
let bucket = cachedHookProviders.get(env);
if (!bucket) {
bucket = new Map<string, ProviderPlugin[]>();
cachedHookProviders.set(env, bucket);
}
return bucket;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === "object" && !Array.isArray(value);
}
function projectPluginEntryForProviderHookCache(
pluginId: string,
entry: unknown,
fullConfigPluginIds: ReadonlySet<string>,
): unknown {
if (!isRecord(entry) || fullConfigPluginIds.has(pluginId)) {
return entry;
}
const {
config: _config,
hooks: _hooks,
subagent: _subagent,
apiKey: _apiKey,
env: _env,
...rest
} = entry;
return rest;
}
function projectPluginsConfigForProviderHookCache(
plugins: OpenClawConfig["plugins"],
fullConfigPluginIds: ReadonlySet<string>,
): unknown {
if (!isRecord(plugins)) {
return plugins ?? null;
}
const entries = isRecord(plugins.entries)
? Object.fromEntries(
Object.entries(plugins.entries)
.toSorted(([left], [right]) => left.localeCompare(right))
.map(([pluginId, entry]) => [
pluginId,
projectPluginEntryForProviderHookCache(pluginId, entry, fullConfigPluginIds),
]),
)
: plugins.entries;
return {
...plugins,
entries,
};
}
function resolveProviderOwnerConfigPluginIds(params: {
providerRefs?: readonly string[];
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
}): string[] {
if (!params.providerRefs?.length) {
return [];
}
const pluginIds = new Set<string>();
for (const provider of params.providerRefs) {
for (const pluginId of resolveOwningPluginIdsForProvider({
provider,
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
}) ?? []) {
pluginIds.add(pluginId);
}
const apiOwnerHint = resolveProviderConfigApiOwnerHint({
provider,
config: params.config,
});
if (!apiOwnerHint) {
continue;
}
for (const pluginId of resolveOwningPluginIdsForProvider({
provider: apiOwnerHint,
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
}) ?? []) {
pluginIds.add(pluginId);
}
}
return [...pluginIds].toSorted((left, right) => left.localeCompare(right));
}
export function resolveProviderHookConfigCacheShape(
config: OpenClawConfig | undefined,
fullConfigPluginIds: readonly string[] | undefined,
): unknown {
if (!config) {
return null;
}
const fullConfigPluginIdSet = new Set(fullConfigPluginIds ?? []);
return {
plugins: projectPluginsConfigForProviderHookCache(config.plugins, fullConfigPluginIdSet),
};
}
function buildHookProviderCacheKey(params: {
config?: OpenClawConfig;
workspaceDir?: string;
onlyPluginIds?: string[];
providerRefs?: string[];
env?: NodeJS.ProcessEnv;
fullConfigPluginIds?: string[];
applyAutoEnable?: boolean;
bundledProviderAllowlistCompat?: boolean;
bundledProviderVitestCompat?: boolean;
installBundledRuntimeDeps?: boolean;
}) {
const { roots } = resolvePluginCacheInputs({
workspaceDir: params.workspaceDir,
env: params.env,
});
const onlyPluginIds = normalizePluginIdScope(params.onlyPluginIds);
const loadPolicy = {
applyAutoEnable: params.applyAutoEnable ?? true,
bundledProviderAllowlistCompat: params.bundledProviderAllowlistCompat ?? true,
bundledProviderVitestCompat: params.bundledProviderVitestCompat ?? true,
installBundledRuntimeDeps: params.installBundledRuntimeDeps ?? false,
};
return `${roots.workspace ?? ""}::${roots.global}::${roots.stock ?? ""}::${JSON.stringify(resolveProviderHookConfigCacheShape(params.config, params.fullConfigPluginIds))}::${serializePluginIdScope(onlyPluginIds)}::${JSON.stringify(params.providerRefs ?? [])}::${JSON.stringify(loadPolicy)}`;
}
export function clearProviderRuntimeHookCache(): void {
cachedHookProviders = new WeakMap<NodeJS.ProcessEnv, Map<string, ProviderPlugin[]>>();
}
export function resetProviderRuntimeHookCacheForTest(): void {
clearProviderRuntimeHookCache();
}
export const __testing = {
buildHookProviderCacheKey,
} as const;
export function resolveProviderPluginsForHooks(params: {
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
onlyPluginIds?: string[];
providerRefs?: string[];
applyAutoEnable?: boolean;
bundledProviderAllowlistCompat?: boolean;
bundledProviderVitestCompat?: boolean;
installBundledRuntimeDeps?: boolean;
}): ProviderPlugin[] {
const env = params.env ?? process.env;
const workspaceDir = params.workspaceDir ?? getActivePluginRegistryWorkspaceDirFromState();
const cacheBucket = resolveHookProviderCacheBucket(env);
const onlyPluginIds = normalizePluginIdScope(params.onlyPluginIds);
const explicitPluginIds = onlyPluginIds ?? [];
const fullConfigPluginIds = [
...new Set([
...explicitPluginIds,
...resolveProviderOwnerConfigPluginIds({
providerRefs: params.providerRefs,
config: params.config,
workspaceDir,
env,
}),
]),
].toSorted((left, right) => left.localeCompare(right));
const cacheKey = buildHookProviderCacheKey({
config: params.config,
workspaceDir,
onlyPluginIds,
providerRefs: params.providerRefs,
env,
fullConfigPluginIds,
applyAutoEnable: params.applyAutoEnable,
bundledProviderAllowlistCompat: params.bundledProviderAllowlistCompat,
bundledProviderVitestCompat: params.bundledProviderVitestCompat,
installBundledRuntimeDeps: params.installBundledRuntimeDeps,
});
const cached = cacheBucket.get(cacheKey);
if (cached) {
return cached;
}
if (
isPluginProvidersLoadInFlight({
...params,
workspaceDir,
env,
activate: false,
cache: false,
applyAutoEnable: params.applyAutoEnable,
bundledProviderAllowlistCompat: params.bundledProviderAllowlistCompat ?? true,
bundledProviderVitestCompat: params.bundledProviderVitestCompat ?? true,
installBundledRuntimeDeps: params.installBundledRuntimeDeps,
})
) {
return [];
}
const resolved = resolvePluginProviders({
...params,
workspaceDir,
env,
activate: false,
cache: false,
applyAutoEnable: params.applyAutoEnable,
bundledProviderAllowlistCompat: params.bundledProviderAllowlistCompat ?? true,
bundledProviderVitestCompat: params.bundledProviderVitestCompat ?? true,
installBundledRuntimeDeps: params.installBundledRuntimeDeps,
});
cacheBucket.set(cacheKey, resolved);
return resolved;
}
export function resolveProviderRuntimePlugin(params: {
provider: string;
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
applyAutoEnable?: boolean;
bundledProviderAllowlistCompat?: boolean;
bundledProviderVitestCompat?: boolean;
installBundledRuntimeDeps?: boolean;
}): ProviderPlugin | undefined {
const apiOwnerHint = resolveProviderConfigApiOwnerHint({
provider: params.provider,
config: params.config,
});
return resolveProviderPluginsForHooks({
config: params.config,
workspaceDir: params.workspaceDir ?? getActivePluginRegistryWorkspaceDirFromState(),
env: params.env,
providerRefs: apiOwnerHint ? [params.provider, apiOwnerHint] : [params.provider],
applyAutoEnable: params.applyAutoEnable,
bundledProviderAllowlistCompat: params.bundledProviderAllowlistCompat,
bundledProviderVitestCompat: params.bundledProviderVitestCompat,
installBundledRuntimeDeps: params.installBundledRuntimeDeps,
}).find((plugin) => {
if (apiOwnerHint) {
return (
matchesProviderLiteralId(plugin, params.provider) || matchesProviderId(plugin, apiOwnerHint)
);
}
return matchesProviderId(plugin, params.provider);
});
}
export function resolveProviderHookPlugin(params: {
provider: string;
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
}): ProviderPlugin | undefined {
return (
resolveProviderRuntimePlugin(params) ??
resolveProviderPluginsForHooks({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
}).find((candidate) => matchesProviderId(candidate, params.provider))
);
}
export function prepareProviderExtraParams(params: {
provider: string;
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
context: ProviderPrepareExtraParamsContext;
}) {
return resolveProviderRuntimePlugin(params)?.prepareExtraParams?.(params.context) ?? undefined;
}
export function resolveProviderExtraParamsForTransport(params: {
provider: string;
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
context: ProviderExtraParamsForTransportContext;
}) {
return resolveProviderHookPlugin(params)?.extraParamsForTransport?.(params.context) ?? undefined;
}
export function resolveProviderAuthProfileId(params: {
provider: string;
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
context: ProviderResolveAuthProfileIdContext;
}): string | undefined {
const resolved = resolveProviderHookPlugin(params)?.resolveAuthProfileId?.(params.context);
return typeof resolved === "string" && resolved.trim() ? resolved.trim() : undefined;
}
export function resolveProviderFollowupFallbackRoute(params: {
provider: string;
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
context: ProviderFollowupFallbackRouteContext;
}): ProviderFollowupFallbackRouteResult | undefined {
return resolveProviderHookPlugin(params)?.followupFallbackRoute?.(params.context) ?? undefined;
}
export function wrapProviderStreamFn(params: {
provider: string;
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
context: ProviderWrapStreamFnContext;
}) {
return resolveProviderHookPlugin(params)?.wrapStreamFn?.(params.context) ?? undefined;
}