From 5da34a982b8dec8f77d3ee3ecb4079c9cb6df711 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 28 May 2026 08:43:40 +0100 Subject: [PATCH] perf: avoid runtime catalog load for reasoning defaults --- src/auto-reply/reply/model-selection.test.ts | 26 +++++++++++++- src/auto-reply/reply/model-selection.ts | 38 +++++++++++++++++--- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/src/auto-reply/reply/model-selection.test.ts b/src/auto-reply/reply/model-selection.test.ts index 77142577abb..65416852b8d 100644 --- a/src/auto-reply/reply/model-selection.test.ts +++ b/src/auto-reply/reply/model-selection.test.ts @@ -54,8 +54,8 @@ vi.mock("../../agents/auth-profiles.runtime.js", () => ({ afterEach(() => { MODEL_CONTEXT_TOKEN_CACHE.clear(); + vi.mocked(loadManifestModelCatalog).mockReset(); vi.mocked(loadManifestModelCatalog).mockReturnValue([]); - vi.mocked(loadManifestModelCatalog).mockClear(); authProfileStoreMock.reset(); }); @@ -1524,6 +1524,30 @@ describe("createModelSelectionState auto-failover overrides", () => { }); describe("createModelSelectionState resolveDefaultReasoningLevel", () => { + it("uses manifest metadata before hydrating the runtime reasoning catalog", async () => { + vi.mocked(loadModelCatalog).mockClear(); + vi.mocked(loadManifestModelCatalog).mockClear(); + vi.mocked(loadManifestModelCatalog).mockReturnValueOnce([ + { provider: "local", id: "fast-reasoner", name: "Fast Reasoner", reasoning: true }, + ]); + const state = await createModelSelectionState({ + cfg: {} as OpenClawConfig, + agentCfg: undefined, + defaultProvider: "local", + defaultModel: "fast-reasoner", + provider: "local", + model: "fast-reasoner", + hasModelDirective: false, + }); + + await expect(state.resolveDefaultReasoningLevel()).resolves.toBe("on"); + expect(loadManifestModelCatalog).toHaveBeenCalledWith({ + config: {}, + fallbackToMetadataScan: false, + }); + expect(loadModelCatalog).not.toHaveBeenCalled(); + }); + it("returns on when catalog model has reasoning true", async () => { const { loadModelCatalog } = await import("../../agents/model-catalog.runtime.js"); vi.mocked(loadModelCatalog).mockResolvedValueOnce([ diff --git a/src/auto-reply/reply/model-selection.ts b/src/auto-reply/reply/model-selection.ts index 0d9a1048fbc..c53c514f81e 100644 --- a/src/auto-reply/reply/model-selection.ts +++ b/src/auto-reply/reply/model-selection.ts @@ -388,7 +388,7 @@ export async function createModelSelectionState(params: { agentId: params.agentId, ...RUNTIME_MODEL_VISIBILITY_NORMALIZATION, }).allowedCatalog; - const loadManifestCatalogForThinking = async () => { + const loadManifestCatalog = async () => { if (manifestModelCatalog) { return manifestModelCatalog; } @@ -397,7 +397,7 @@ export async function createModelSelectionState(params: { config: cfg, fallbackToMetadataScan: false, }); - logStage("manifest-catalog-loaded-for-thinking", `entries=${manifestModelCatalog.length}`); + logStage("manifest-catalog-loaded", `entries=${manifestModelCatalog.length}`); return manifestModelCatalog; }; const resolveThinkingCatalog = async () => { @@ -419,7 +419,7 @@ export async function createModelSelectionState(params: { // allowlist rows know only provider/id; manifest rows can prove reasoning // support without opening the Pi auth-backed model registry. if (!modelCatalog && selectedCatalogEntry?.reasoning === undefined) { - const manifestCatalog = buildThinkingCatalog(await loadManifestCatalogForThinking()); + const manifestCatalog = buildThinkingCatalog(await loadManifestCatalog()); const manifestSelectedEntry = findSelectedCatalogEntry({ catalog: manifestCatalog, provider, @@ -475,18 +475,46 @@ export async function createModelSelectionState(params: { return defaultThinkingLevel; }; + let defaultReasoningLevel: "on" | "off" | undefined; const resolveDefaultReasoningLevel = async (): Promise<"on" | "off"> => { + if (defaultReasoningLevel) { + return defaultReasoningLevel; + } let catalogForReasoning = modelCatalog ?? allowedModelCatalog; - if (!catalogForReasoning || catalogForReasoning.length === 0) { + let selectedReasoningEntry = findSelectedCatalogEntry({ + catalog: catalogForReasoning, + provider, + model, + }); + if (!modelCatalog && selectedReasoningEntry?.reasoning === undefined) { + const manifestCatalog = await loadManifestCatalog(); + const manifestReasoningCatalog = hasAllowlist + ? buildThinkingCatalog(manifestCatalog) + : manifestCatalog; + const manifestSelectedEntry = findSelectedCatalogEntry({ + catalog: manifestReasoningCatalog, + provider, + model, + }); + if (manifestSelectedEntry?.reasoning !== undefined) { + catalogForReasoning = manifestReasoningCatalog; + selectedReasoningEntry = manifestSelectedEntry; + } + } + if ( + (!catalogForReasoning || catalogForReasoning.length === 0) && + selectedReasoningEntry?.reasoning === undefined + ) { modelCatalog = await (await loadModelCatalogRuntime()).loadModelCatalog({ config: cfg }); logStage("catalog-loaded-for-reasoning", `entries=${modelCatalog.length}`); catalogForReasoning = modelCatalog; } - return resolveReasoningDefault({ + defaultReasoningLevel = resolveReasoningDefault({ provider, model, catalog: catalogForReasoning, }); + return defaultReasoningLevel; }; return {