From 354084b1b320a2a8c60b841ff19f67d8c7d19972 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Fri, 1 May 2026 07:34:08 +0530 Subject: [PATCH] fix(providers): cache targeted runtime hook resolution --- src/plugins/provider-hook-runtime.ts | 75 ++++++++++++++++++++++------ src/plugins/provider-runtime.test.ts | 64 +++++++++++++++++++++++- src/plugins/provider-runtime.ts | 17 ++++--- 3 files changed, 131 insertions(+), 25 deletions(-) diff --git a/src/plugins/provider-hook-runtime.ts b/src/plugins/provider-hook-runtime.ts index 07140865ef9..0f18888b35e 100644 --- a/src/plugins/provider-hook-runtime.ts +++ b/src/plugins/provider-hook-runtime.ts @@ -14,6 +14,22 @@ import type { ProviderWrapStreamFnContext, } from "./types.js"; +const providerRuntimePluginCache = new WeakMap< + OpenClawConfig, + Map +>(); + +type ProviderRuntimePluginLookupParams = { + provider: string; + config?: OpenClawConfig; + workspaceDir?: string; + env?: NodeJS.ProcessEnv; + applyAutoEnable?: boolean; + bundledProviderAllowlistCompat?: boolean; + bundledProviderVitestCompat?: boolean; + installBundledRuntimeDeps?: boolean; +}; + function matchesProviderId(provider: ProviderPlugin, providerId: string): boolean { const normalized = normalizeProviderId(providerId); if (!normalized) { @@ -27,6 +43,33 @@ function matchesProviderId(provider: ProviderPlugin, providerId: string): boolea ); } +function resolveProviderRuntimePluginCacheKey(params: ProviderRuntimePluginLookupParams): string { + return JSON.stringify({ + provider: normalizeLowercaseStringOrEmpty(params.provider), + plugins: params.config?.plugins, + models: params.config?.models?.providers, + workspaceDir: params.workspaceDir ?? "", + applyAutoEnable: params.applyAutoEnable ?? null, + bundledProviderAllowlistCompat: params.bundledProviderAllowlistCompat ?? null, + bundledProviderVitestCompat: params.bundledProviderVitestCompat ?? null, + installBundledRuntimeDeps: params.installBundledRuntimeDeps ?? null, + }); +} + +function resolveProviderRuntimePluginCache( + params: ProviderRuntimePluginLookupParams, +): Map | undefined { + if (!params.config || (params.env && params.env !== process.env)) { + return undefined; + } + let cache = providerRuntimePluginCache.get(params.config); + if (!cache) { + cache = new Map(); + providerRuntimePluginCache.set(params.config, cache); + } + return cache; +} + function matchesProviderLiteralId(provider: ProviderPlugin, providerId: string): boolean { const normalized = normalizeLowercaseStringOrEmpty(providerId); return !!normalized && normalizeLowercaseStringOrEmpty(provider.id) === normalized; @@ -51,7 +94,6 @@ export function resolveProviderPluginsForHooks(params: { workspaceDir, env, activate: false, - cache: false, applyAutoEnable: params.applyAutoEnable, bundledProviderAllowlistCompat: params.bundledProviderAllowlistCompat ?? true, bundledProviderVitestCompat: params.bundledProviderVitestCompat ?? true, @@ -65,7 +107,6 @@ export function resolveProviderPluginsForHooks(params: { workspaceDir, env, activate: false, - cache: false, applyAutoEnable: params.applyAutoEnable, bundledProviderAllowlistCompat: params.bundledProviderAllowlistCompat ?? true, bundledProviderVitestCompat: params.bundledProviderVitestCompat ?? true, @@ -74,21 +115,19 @@ export function resolveProviderPluginsForHooks(params: { 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 { +export function resolveProviderRuntimePlugin( + params: ProviderRuntimePluginLookupParams, +): ProviderPlugin | undefined { + const cache = resolveProviderRuntimePluginCache(params); + const cacheKey = cache ? resolveProviderRuntimePluginCacheKey(params) : ""; + if (cache?.has(cacheKey)) { + return cache.get(cacheKey) ?? undefined; + } const apiOwnerHint = resolveProviderConfigApiOwnerHint({ provider: params.provider, config: params.config, }); - return resolveProviderPluginsForHooks({ + const plugin = resolveProviderPluginsForHooks({ config: params.config, workspaceDir: params.workspaceDir ?? getActivePluginRegistryWorkspaceDirFromState(), env: params.env, @@ -105,6 +144,8 @@ export function resolveProviderRuntimePlugin(params: { } return matchesProviderId(plugin, params.provider); }); + cache?.set(cacheKey, plugin ?? null); + return plugin; } export function resolveProviderHookPlugin(params: { @@ -140,7 +181,9 @@ export function resolveProviderExtraParamsForTransport(params: { env?: NodeJS.ProcessEnv; context: ProviderExtraParamsForTransportContext; }) { - return resolveProviderHookPlugin(params)?.extraParamsForTransport?.(params.context) ?? undefined; + return ( + resolveProviderRuntimePlugin(params)?.extraParamsForTransport?.(params.context) ?? undefined + ); } export function resolveProviderAuthProfileId(params: { @@ -150,7 +193,7 @@ export function resolveProviderAuthProfileId(params: { env?: NodeJS.ProcessEnv; context: ProviderResolveAuthProfileIdContext; }): string | undefined { - const resolved = resolveProviderHookPlugin(params)?.resolveAuthProfileId?.(params.context); + const resolved = resolveProviderRuntimePlugin(params)?.resolveAuthProfileId?.(params.context); return typeof resolved === "string" && resolved.trim() ? resolved.trim() : undefined; } @@ -171,5 +214,5 @@ export function wrapProviderStreamFn(params: { env?: NodeJS.ProcessEnv; context: ProviderWrapStreamFnContext; }) { - return resolveProviderHookPlugin(params)?.wrapStreamFn?.(params.context) ?? undefined; + return resolveProviderRuntimePlugin(params)?.wrapStreamFn?.(params.context) ?? undefined; } diff --git a/src/plugins/provider-runtime.test.ts b/src/plugins/provider-runtime.test.ts index ef439eaa92c..acb820ceb29 100644 --- a/src/plugins/provider-runtime.test.ts +++ b/src/plugins/provider-runtime.test.ts @@ -1114,6 +1114,69 @@ describe("provider-runtime", () => { ).toBe(wrappedStreamFn); }); + it("does not run broad provider-hook scans for reasoning output mode", () => { + resolvePluginProvidersMock.mockImplementation((params) => { + if (params.providerRefs?.includes("mock-openai")) { + return []; + } + throw new Error("unexpected broad provider hook scan"); + }); + + expect( + resolveProviderReasoningOutputModeWithPlugin({ + provider: "mock-openai", + context: createDemoResolvedModelContext({ + provider: "mock-openai", + modelId: "gpt-5.5", + }), + }), + ).toBeUndefined(); + expect(resolvePluginProvidersMock).toHaveBeenCalledOnce(); + }); + + it("does not run broad provider-hook scans for auth profile selection", () => { + resolvePluginProvidersMock.mockImplementation((params) => { + if (params.providerRefs?.includes("mock-openai")) { + return []; + } + throw new Error("unexpected broad provider hook scan"); + }); + + expect( + resolveProviderAuthProfileId({ + provider: "mock-openai", + context: createDemoRuntimeContext({ + provider: "mock-openai", + modelId: "gpt-5.5", + profileOrder: [], + authStore: { version: 1, profiles: {}, order: {} }, + }), + }), + ).toBeUndefined(); + expect(resolvePluginProvidersMock).toHaveBeenCalledOnce(); + }); + + it("does not run broad provider-hook scans for transport extra params", () => { + resolvePluginProvidersMock.mockImplementation((params) => { + if (params.providerRefs?.includes("mock-openai")) { + return []; + } + throw new Error("unexpected broad provider hook scan"); + }); + + expect( + resolveProviderExtraParamsForTransport({ + provider: "mock-openai", + context: createDemoRuntimeContext({ + provider: "mock-openai", + modelId: "gpt-5.5", + extraParams: {}, + }), + }), + ).toBeUndefined(); + expect(resolvePluginProvidersMock).toHaveBeenCalledOnce(); + }); + it("normalizes transport hooks without needing provider ownership", () => { resolvePluginProvidersMock.mockReturnValue([ { @@ -1969,7 +2032,6 @@ describe("provider-runtime", () => { expect.objectContaining({ onlyPluginIds: ["openai"], activate: false, - cache: false, }), ); expect(resolveCatalogHookProviderPluginIdsMock).toHaveBeenCalledTimes(1); diff --git a/src/plugins/provider-runtime.ts b/src/plugins/provider-runtime.ts index b357d36fb93..2edabae1a1c 100644 --- a/src/plugins/provider-runtime.ts +++ b/src/plugins/provider-runtime.ts @@ -525,7 +525,7 @@ export function resolveProviderReplayPolicyWithPlugin(params: { env?: NodeJS.ProcessEnv; context: ProviderReplayPolicyContext; }): ProviderReplayPolicy | undefined { - return resolveProviderHookPlugin(params)?.buildReplayPolicy?.(params.context) ?? undefined; + return resolveProviderRuntimePlugin(params)?.buildReplayPolicy?.(params.context) ?? undefined; } export async function sanitizeProviderReplayHistoryWithPlugin(params: { @@ -535,7 +535,7 @@ export async function sanitizeProviderReplayHistoryWithPlugin(params: { env?: NodeJS.ProcessEnv; context: ProviderSanitizeReplayHistoryContext; }) { - return await resolveProviderHookPlugin(params)?.sanitizeReplayHistory?.(params.context); + return await resolveProviderRuntimePlugin(params)?.sanitizeReplayHistory?.(params.context); } export async function validateProviderReplayTurnsWithPlugin(params: { @@ -545,7 +545,7 @@ export async function validateProviderReplayTurnsWithPlugin(params: { env?: NodeJS.ProcessEnv; context: ProviderValidateReplayTurnsContext; }) { - return await resolveProviderHookPlugin(params)?.validateReplayTurns?.(params.context); + return await resolveProviderRuntimePlugin(params)?.validateReplayTurns?.(params.context); } export function normalizeProviderToolSchemasWithPlugin(params: { @@ -555,7 +555,7 @@ export function normalizeProviderToolSchemasWithPlugin(params: { env?: NodeJS.ProcessEnv; context: ProviderNormalizeToolSchemasContext; }) { - return resolveProviderHookPlugin(params)?.normalizeToolSchemas?.(params.context) ?? undefined; + return resolveProviderRuntimePlugin(params)?.normalizeToolSchemas?.(params.context) ?? undefined; } export function inspectProviderToolSchemasWithPlugin(params: { @@ -565,7 +565,7 @@ export function inspectProviderToolSchemasWithPlugin(params: { env?: NodeJS.ProcessEnv; context: ProviderNormalizeToolSchemasContext; }) { - return resolveProviderHookPlugin(params)?.inspectToolSchemas?.(params.context) ?? undefined; + return resolveProviderRuntimePlugin(params)?.inspectToolSchemas?.(params.context) ?? undefined; } export function resolveProviderReasoningOutputModeWithPlugin(params: { @@ -575,7 +575,7 @@ export function resolveProviderReasoningOutputModeWithPlugin(params: { env?: NodeJS.ProcessEnv; context: ProviderReasoningOutputModeContext; }): ProviderReasoningOutputMode | undefined { - const mode = resolveProviderHookPlugin(params)?.resolveReasoningOutputMode?.(params.context); + const mode = resolveProviderRuntimePlugin(params)?.resolveReasoningOutputMode?.(params.context); return mode === "native" || mode === "tagged" ? mode : undefined; } @@ -597,7 +597,7 @@ export function resolveProviderTransportTurnStateWithPlugin(params: { context: ProviderResolveTransportTurnStateContext; }): ProviderTransportTurnState | undefined { return ( - resolveProviderHookPlugin(params)?.resolveTransportTurnState?.(params.context) ?? undefined + resolveProviderRuntimePlugin(params)?.resolveTransportTurnState?.(params.context) ?? undefined ); } @@ -609,7 +609,8 @@ export function resolveProviderWebSocketSessionPolicyWithPlugin(params: { context: ProviderResolveWebSocketSessionPolicyContext; }): ProviderWebSocketSessionPolicy | undefined { return ( - resolveProviderHookPlugin(params)?.resolveWebSocketSessionPolicy?.(params.context) ?? undefined + resolveProviderRuntimePlugin(params)?.resolveWebSocketSessionPolicy?.(params.context) ?? + undefined ); }