Files
openclaw/src/agents/model-selection.plugin-runtime.test.ts
T
Peter Steinberger 7a8eee4a36 perf(agents): keep turn-path model catalog reads off the full live build (#120834)
* perf(agents): keep turn-path model catalog reads off the full live build

First agent turns (embedded and cron) resolved thinking capability through
loadPreparedModelCatalogSnapshot without readOnly, which materialized the
full live model-runtime catalog: ambient synthetic-auth discovery fanned out
to every registered provider and loaded plugin discovery modules through
jiti source transform (3,172 TS modules, 36s event-loop block, +600MB heap,
58.7s model-selection on a cold gateway).

- add loadProviderScopedThinkingCatalog: manifest metadata first, then a
  provider-scoped read-only static catalog, then scoped live discovery only
  for runtime-discovery providers (preserves #116584 Ollama semantics)
- route scopedLiveProviderDiscovery through the scoped read-only loader
- scope live-mode ambient synthetic-auth refs to the requested providers
- bound the last-resort synthetic-auth sweep to discovery entry modules
- memoize per-turn plugin skill dir resolution/republish (single-slot,
  lifecycle-cleared; was a full walk + symlink republish every turn)

Cold first turn 72.7s -> ~22s wall (remaining cost is provider prefill of
the ~19.5k-token default prompt); model-selection 58,726ms -> 124ms.

* test(agents): align model-catalog.runtime mocks with scoped thinking catalog seam

Explicit vi.mock factories must export every binding prod touches; the new
loadProviderScopedThinkingCatalog export is now mocked everywhere the module
is stubbed, and the live-model-switch Ollama hydration test asserts the new
provider-scoped seam instead of the retired unscoped snapshot call shape.

* test(agents): export scoped thinking catalog from every prepared-catalog mock; split synthetic-auth helpers

- add loadProviderScopedThinkingCatalog to all explicit prepared-model-catalog
  and model-catalog.runtime mock factories (vi.mock factories must export every
  binding prod touches)
- move synthetic-auth ref scoping/resolution into
  prepared-model-runtime.synthetic-auth.ts; keeps facts under the max-lines cap

* test(agents): prove scoped thinking hydration for runtime-only models

Boundary proof for the ClawSweeper review gap: the three-tier helper stops at
manifest or scoped-static when they resolve, and runs provider-scoped live
discovery (no broad fanout) only for runtime-only models; cron selection
hydrates through the same scoped helper and skips it entirely for thinking=off.

* test(agents): accept rest args in scoped thinking catalog mocks
2026-08-08 22:48:40 -07:00

439 lines
15 KiB
TypeScript

// Covers plugin-owned model id normalization through selection surfaces.
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const normalizeProviderModelIdWithPluginMock = vi.fn();
const emptyPluginMetadataSnapshot = vi.hoisted(() => ({
configFingerprint: "model-selection-plugin-runtime-test-empty-plugin-metadata",
plugins: [
{
modelIdNormalization: {
providers: {
google: {
aliases: {
"gemini-3.1-pro": "gemini-3.1-pro-preview",
},
},
},
},
},
],
}));
const getCurrentPluginMetadataSnapshotMock = vi.hoisted(() => vi.fn());
const loadPreparedModelCatalogSnapshotMock = vi.hoisted(() => vi.fn());
vi.mock("./provider-model-normalization.runtime.js", () => ({
normalizeProviderModelIdWithRuntime: (params: unknown) =>
normalizeProviderModelIdWithPluginMock(params),
}));
vi.mock("../plugins/current-plugin-metadata-snapshot.js", () => ({
getCurrentPluginMetadataSnapshot: getCurrentPluginMetadataSnapshotMock,
}));
vi.mock("./model-catalog.runtime.js", () => ({
loadManifestModelCatalog: () => [],
loadProviderScopedThinkingCatalog: async () => [],
loadPreparedModelCatalog: async () => [],
loadPreparedModelCatalogSnapshot: loadPreparedModelCatalogSnapshotMock,
}));
let createModelSelectionStateForTest: typeof import("../auto-reply/reply/model-selection.js").createModelSelectionState;
describe("model-selection plugin runtime normalization", () => {
beforeAll(async () => {
({ createModelSelectionState: createModelSelectionStateForTest } =
await import("../auto-reply/reply/model-selection.js"));
});
beforeEach(() => {
normalizeProviderModelIdWithPluginMock.mockReset();
getCurrentPluginMetadataSnapshotMock.mockReset();
getCurrentPluginMetadataSnapshotMock.mockReturnValue(emptyPluginMetadataSnapshot);
loadPreparedModelCatalogSnapshotMock.mockReset();
loadPreparedModelCatalogSnapshotMock.mockResolvedValue({ entries: [], authoritative: true });
});
it("delegates provider-owned model id normalization to plugin runtime hooks", async () => {
normalizeProviderModelIdWithPluginMock.mockImplementation(({ provider, context }) => {
if (
provider === "custom-provider" &&
(context as { modelId?: string }).modelId === "custom-legacy-model"
) {
return "custom-modern-model";
}
return undefined;
});
const { parseModelRef } = await import("./model-selection.js");
expect(parseModelRef("custom-legacy-model", "custom-provider")).toEqual({
provider: "custom-provider",
model: "custom-modern-model",
});
expect(normalizeProviderModelIdWithPluginMock).toHaveBeenCalledWith({
provider: "custom-provider",
context: {
provider: "custom-provider",
modelId: "custom-legacy-model",
},
});
});
it("keeps static normalization while skipping plugin runtime hooks when disabled", async () => {
const { parseModelRef } = await import("./model-selection.js");
expect(
parseModelRef("gemini-3.1-pro", "google", {
allowPluginNormalization: false,
}),
).toEqual({
provider: "google",
model: "gemini-3.1-pro-preview",
});
expect(normalizeProviderModelIdWithPluginMock).not.toHaveBeenCalled();
});
it("keeps provider plugin normalization when inferring provider for bare defaults", async () => {
normalizeProviderModelIdWithPluginMock.mockImplementation(({ provider, context }) => {
if (
provider === "custom-provider" &&
(context as { modelId?: string }).modelId === "custom-legacy-model"
) {
return "custom-modern-model";
}
return undefined;
});
const { resolveConfiguredModelRef } = await import("./model-selection.js");
expect(
resolveConfiguredModelRef({
cfg: {
agents: {
defaults: {
model: { primary: "custom-legacy-model" },
models: {
"custom-provider/custom-legacy-model": {},
},
},
},
},
defaultProvider: "openai",
defaultModel: "gpt-5.5",
}),
).toEqual({
provider: "custom-provider",
model: "custom-modern-model",
});
});
it("keeps model visibility policy construction off plugin runtime hooks by default", async () => {
// Visibility policy is a hot/static path. It preserves configured keys
// unless callers explicitly opt into runtime plugin normalization.
normalizeProviderModelIdWithPluginMock.mockImplementation(({ provider, context }) => {
if (
provider === "custom-provider" &&
(context as { modelId?: string }).modelId === "custom-legacy-model"
) {
return "custom-modern-model";
}
return undefined;
});
const { createModelVisibilityPolicy } = await import("./model-visibility-policy.js");
const policy = createModelVisibilityPolicy({
cfg: {
agents: {
defaults: {
models: {
"custom-provider/custom-legacy-model": {},
},
},
},
},
catalog: [],
defaultProvider: "custom-provider",
defaultModel: "custom-legacy-model",
});
expect(policy.allowedKeys.has("custom-provider/custom-legacy-model")).toBe(true);
expect(policy.allowedKeys.has("custom-provider/custom-modern-model")).toBe(false);
expect(normalizeProviderModelIdWithPluginMock).not.toHaveBeenCalled();
});
it("propagates explicit plugin runtime normalization opt-in through model visibility policy", async () => {
normalizeProviderModelIdWithPluginMock.mockImplementation(({ provider, context }) => {
if (
provider === "custom-provider" &&
(context as { modelId?: string }).modelId === "custom-legacy-model"
) {
return "custom-modern-model";
}
return undefined;
});
const { createModelVisibilityPolicy } = await import("./model-visibility-policy.js");
const policy = createModelVisibilityPolicy({
cfg: {
agents: {
defaults: {
models: {
"custom-provider/custom-legacy-model": {},
},
},
},
},
catalog: [],
defaultProvider: "custom-provider",
defaultModel: "custom-legacy-model",
allowPluginNormalization: true,
});
expect(policy.allowedKeys.has("custom-provider/custom-modern-model")).toBe(true);
expect(normalizeProviderModelIdWithPluginMock).toHaveBeenCalled();
});
it("keeps plugin-normalized stored overrides allowed in auto-reply runtime selection", async () => {
// Stored session overrides are runtime inputs, so provider-owned
// normalization keeps old persisted ids usable without resetting them.
normalizeProviderModelIdWithPluginMock.mockImplementation(({ provider, context }) => {
if (
provider === "custom-provider" &&
(context as { modelId?: string }).modelId === "custom-legacy-model"
) {
return "custom-modern-model";
}
return undefined;
});
const cfg = {
agents: {
defaults: {
models: {
"custom-provider/custom-legacy-model": {},
},
},
},
};
const sessionKey = "agent:main:discord:channel:c1";
const sessionEntry = {
sessionId: sessionKey,
updatedAt: 1,
providerOverride: "custom-provider",
modelOverride: "custom-legacy-model",
};
const sessionStore = { [sessionKey]: sessionEntry };
const state = await createModelSelectionStateForTest({
cfg,
agentCfg: cfg.agents.defaults,
sessionEntry,
sessionStore,
sessionKey,
defaultProvider: "custom-provider",
defaultModel: "custom-legacy-model",
provider: "custom-provider",
model: "custom-legacy-model",
hasModelDirective: false,
});
expect(state.provider).toBe("custom-provider");
expect(state.model).toBe("custom-modern-model");
expect(state.resetModelOverride).toBe(false);
});
it("reuses one lifecycle metadata snapshot across auto-reply model normalization", async () => {
normalizeProviderModelIdWithPluginMock.mockReturnValue(undefined);
const configuredRefs = Object.fromEntries(
Array.from({ length: 20 }, (_, index) => [`custom-provider/model-${index}`, {}]),
);
const cfg = {
agents: {
defaults: {
modelPolicy: { allow: Object.keys(configuredRefs) },
models: configuredRefs,
},
},
};
const state = await createModelSelectionStateForTest({
cfg,
agentCfg: cfg.agents.defaults,
defaultProvider: "custom-provider",
defaultModel: "model-0",
provider: "custom-provider",
model: "model-0",
hasModelDirective: false,
});
expect(state.allowedModelCatalog).toHaveLength(20);
expect(getCurrentPluginMetadataSnapshotMock).toHaveBeenCalledTimes(1);
expect(getCurrentPluginMetadataSnapshotMock).toHaveBeenCalledWith({
config: cfg,
allowWorkspaceScopedSnapshot: true,
});
});
it("keeps concurrent model-policy runs isolated while sharing metadata", async () => {
normalizeProviderModelIdWithPluginMock.mockReturnValue(undefined);
let signalFirstCatalogLoad: (() => void) | undefined;
let releaseFirstCatalogLoad: (() => void) | undefined;
const firstCatalogLoadStarted = new Promise<void>((resolve) => {
signalFirstCatalogLoad = resolve;
});
const firstCatalogLoadRelease = new Promise<void>((resolve) => {
releaseFirstCatalogLoad = resolve;
});
loadPreparedModelCatalogSnapshotMock
.mockImplementationOnce(async () => {
signalFirstCatalogLoad?.();
await firstCatalogLoadRelease;
return { entries: [], authoritative: true };
})
.mockResolvedValue({ entries: [], authoritative: true });
const createConfig = (model: string) => ({
agents: {
defaults: {
modelPolicy: { allow: [`custom-provider/${model}`] },
models: { [`custom-provider/${model}`]: {} },
},
},
});
const firstConfig = createConfig("first");
const secondConfig = createConfig("second");
const select = (cfg: ReturnType<typeof createConfig>, model: string) =>
createModelSelectionStateForTest({
cfg,
agentCfg: cfg.agents.defaults,
defaultProvider: "custom-provider",
defaultModel: model,
provider: "custom-provider",
model,
hasModelDirective: true,
});
const firstPromise = select(firstConfig, "first");
await firstCatalogLoadStarted;
const secondPromise = select(secondConfig, "second");
await vi.waitFor(() => expect(loadPreparedModelCatalogSnapshotMock).toHaveBeenCalledTimes(2));
releaseFirstCatalogLoad?.();
const [first, second] = await Promise.all([firstPromise, secondPromise]);
expect([...first.allowedModelKeys]).toContain("custom-provider/first");
expect([...first.allowedModelKeys]).not.toContain("custom-provider/second");
expect([...second.allowedModelKeys]).toContain("custom-provider/second");
expect([...second.allowedModelKeys]).not.toContain("custom-provider/first");
expect(getCurrentPluginMetadataSnapshotMock).toHaveBeenCalledTimes(2);
expect(getCurrentPluginMetadataSnapshotMock.mock.calls).toEqual([
[{ config: firstConfig, allowWorkspaceScopedSnapshot: true }],
[{ config: secondConfig, allowWorkspaceScopedSnapshot: true }],
]);
});
it("preserves runtime discovery fallback across configured, stored, and fallback refs", async () => {
getCurrentPluginMetadataSnapshotMock.mockReturnValue(undefined);
const aliases = new Map([
["configured-legacy", "configured-modern"],
["stored-legacy", "stored-modern"],
["fallback-legacy", "fallback-modern"],
]);
normalizeProviderModelIdWithPluginMock.mockImplementation(({ context, plugins }) => {
if (plugins) {
expect(plugins.length).toBeGreaterThan(0);
}
const modelId = (context as { modelId?: string }).modelId ?? "";
return aliases.get(modelId);
});
const cfg = {
agents: {
defaults: {
model: {
primary: "custom-provider/configured-legacy",
fallbacks: ["custom-provider/fallback-legacy"],
},
modelPolicy: {
allow: ["custom-provider/configured-legacy", "custom-provider/stored-legacy"],
},
models: {
"custom-provider/configured-legacy": {},
"custom-provider/stored-legacy": {},
},
},
},
};
const sessionKey = "agent:main:discord:channel:c1";
const sessionEntry = {
sessionId: sessionKey,
updatedAt: 1,
providerOverride: "custom-provider",
modelOverride: "stored-legacy",
};
const state = await createModelSelectionStateForTest({
cfg,
agentCfg: cfg.agents.defaults,
sessionEntry,
sessionStore: { [sessionKey]: sessionEntry },
sessionKey,
defaultProvider: "custom-provider",
defaultModel: "configured-legacy",
provider: "custom-provider",
model: "configured-legacy",
hasModelDirective: false,
});
expect(state.provider).toBe("custom-provider");
expect(state.model).toBe("stored-modern");
expect([...state.allowedModelKeys]).toEqual(
expect.arrayContaining([
"custom-provider/configured-modern",
"custom-provider/stored-modern",
]),
);
expect(getCurrentPluginMetadataSnapshotMock).toHaveBeenCalledWith({
config: cfg,
allowWorkspaceScopedSnapshot: true,
});
expect(
normalizeProviderModelIdWithPluginMock.mock.calls.map(
([call]) => (call as { context?: { modelId?: string } }).context?.modelId,
),
).toEqual(expect.arrayContaining(["configured-legacy", "stored-legacy", "fallback-legacy"]));
});
it("forwards manifestPlugins to the runtime normalization call so it can skip the slot-or-load disk walk", async () => {
normalizeProviderModelIdWithPluginMock.mockReturnValue(undefined);
const preparedPlugins = [
{
modelIdNormalization: {
providers: {
custom: { prefixWhenBare: "prepared" },
},
},
},
];
const { normalizeModelRef } = await import("./model-ref-shared.js");
normalizeModelRef("custom", "my-model", { manifestPlugins: preparedPlugins });
expect(normalizeProviderModelIdWithPluginMock).toHaveBeenCalledWith(
expect.objectContaining({
provider: "custom",
plugins: preparedPlugins,
}),
);
});
it("omits plugins from the runtime call when no manifestPlugins are prepared (preserves current behavior)", async () => {
normalizeProviderModelIdWithPluginMock.mockReturnValue(undefined);
const { normalizeModelRef } = await import("./model-ref-shared.js");
normalizeModelRef("custom", "my-model");
const callArgs = normalizeProviderModelIdWithPluginMock.mock.calls[0]?.[0] as
| { plugins?: unknown }
| undefined;
expect(callArgs).toBeDefined();
expect(callArgs?.plugins).toBeUndefined();
});
});