perf(agents): keep model resolution caches warm

This commit is contained in:
Peter Steinberger
2026-04-28 00:07:46 +01:00
parent e9be25b554
commit c0fdf9923b
6 changed files with 102 additions and 57 deletions

View File

@@ -34,41 +34,26 @@ function matchesProviderLiteralId(provider: ProviderPlugin, providerId: string):
return !!normalized && normalizeLowercaseStringOrEmpty(provider.id) === normalized;
}
let cachedHookProvidersWithoutConfig = new WeakMap<
NodeJS.ProcessEnv,
Map<string, ProviderPlugin[]>
>();
let cachedHookProvidersByConfig = new WeakMap<
OpenClawConfig,
WeakMap<NodeJS.ProcessEnv, Map<string, ProviderPlugin[]>>
>();
let cachedHookProviders = new WeakMap<NodeJS.ProcessEnv, Map<string, ProviderPlugin[]>>();
function resolveHookProviderCacheBucket(params: {
config?: OpenClawConfig;
env: NodeJS.ProcessEnv;
}) {
if (!params.config) {
let bucket = cachedHookProvidersWithoutConfig.get(params.env);
if (!bucket) {
bucket = new Map<string, ProviderPlugin[]>();
cachedHookProvidersWithoutConfig.set(params.env, bucket);
}
return bucket;
}
let envBuckets = cachedHookProvidersByConfig.get(params.config);
if (!envBuckets) {
envBuckets = new WeakMap<NodeJS.ProcessEnv, Map<string, ProviderPlugin[]>>();
cachedHookProvidersByConfig.set(params.config, envBuckets);
}
let bucket = envBuckets.get(params.env);
function resolveHookProviderCacheBucket(env: NodeJS.ProcessEnv) {
let bucket = cachedHookProviders.get(env);
if (!bucket) {
bucket = new Map<string, ProviderPlugin[]>();
envBuckets.set(params.env, bucket);
cachedHookProviders.set(env, bucket);
}
return bucket;
}
function resolveHookProviderConfigCacheShape(config: OpenClawConfig | undefined): unknown {
if (!config) {
return null;
}
return {
plugins: config.plugins,
};
}
function buildHookProviderCacheKey(params: {
config?: OpenClawConfig;
workspaceDir?: string;
@@ -81,18 +66,11 @@ function buildHookProviderCacheKey(params: {
env: params.env,
});
const onlyPluginIds = normalizePluginIdScope(params.onlyPluginIds);
return `${roots.workspace ?? ""}::${roots.global}::${roots.stock ?? ""}::${JSON.stringify(params.config ?? null)}::${serializePluginIdScope(onlyPluginIds)}::${JSON.stringify(params.providerRefs ?? [])}`;
return `${roots.workspace ?? ""}::${roots.global}::${roots.stock ?? ""}::${JSON.stringify(resolveHookProviderConfigCacheShape(params.config))}::${serializePluginIdScope(onlyPluginIds)}::${JSON.stringify(params.providerRefs ?? [])}`;
}
export function clearProviderRuntimeHookCache(): void {
cachedHookProvidersWithoutConfig = new WeakMap<
NodeJS.ProcessEnv,
Map<string, ProviderPlugin[]>
>();
cachedHookProvidersByConfig = new WeakMap<
OpenClawConfig,
WeakMap<NodeJS.ProcessEnv, Map<string, ProviderPlugin[]>>
>();
cachedHookProviders = new WeakMap<NodeJS.ProcessEnv, Map<string, ProviderPlugin[]>>();
}
export function resetProviderRuntimeHookCacheForTest(): void {
@@ -116,10 +94,7 @@ export function resolveProviderPluginsForHooks(params: {
}): ProviderPlugin[] {
const env = params.env ?? process.env;
const workspaceDir = params.workspaceDir ?? getActivePluginRegistryWorkspaceDirFromState();
const cacheBucket = resolveHookProviderCacheBucket({
config: params.config,
env,
});
const cacheBucket = resolveHookProviderCacheBucket(env);
const cacheKey = buildHookProviderCacheKey({
config: params.config,
workspaceDir,

View File

@@ -1,6 +1,6 @@
import type { AgentMessage } from "@mariozechner/pi-agent-core";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { ModelProviderConfig } from "../config/types.js";
import type { ModelProviderConfig, OpenClawConfig } from "../config/types.js";
import type { ProviderRuntimeModel } from "./provider-runtime-model.types.js";
import {
expectAugmentedCodexCatalog,
@@ -503,6 +503,50 @@ describe("provider-runtime", () => {
expect(providerRuntimeWarnMock).not.toHaveBeenCalled();
});
it("reuses catalog hook provider loads when only non-plugin config changes", () => {
resolveCatalogHookProviderPluginIdsMock.mockReturnValue(["demo"]);
resolvePluginProvidersMock.mockReturnValue([
{
id: "demo",
label: "Demo",
auth: [],
suppressBuiltInModel: () => ({ suppress: true, errorMessage: "suppressed" }),
},
]);
const baseConfig = {
plugins: {
entries: {
demo: { enabled: true },
},
},
} as OpenClawConfig;
const firstConfig = {
...baseConfig,
agents: { defaults: { model: "openai/gpt-5.4" } },
} as OpenClawConfig;
const secondConfig = {
...baseConfig,
agents: { defaults: { model: "anthropic/claude-sonnet-4-5" } },
} as OpenClawConfig;
expect(
resolveProviderBuiltInModelSuppression({
config: firstConfig,
env: process.env,
context: { config: firstConfig, env: process.env, provider: "openai", modelId: "demo" },
})?.suppress,
).toBe(true);
expect(
resolveProviderBuiltInModelSuppression({
config: secondConfig,
env: process.env,
context: { config: secondConfig, env: process.env, provider: "openai", modelId: "demo" },
})?.suppress,
).toBe(true);
expect(resolvePluginProvidersMock).toHaveBeenCalledTimes(1);
});
it("returns provider-prepared runtime auth for the matched provider", async () => {
const prepareRuntimeAuth = vi.fn(async () => ({
apiKey: "runtime-token",