From ff9fefb79beac75e9a257aa43fce39db36fa828e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 26 Apr 2026 23:16:00 +0100 Subject: [PATCH] fix(agents): validate thinking with model catalog --- src/agents/agent-command.ts | 14 +++++++++++-- src/auto-reply/thinking.test.ts | 35 +++++++++++++++++++++++++++++++++ src/auto-reply/thinking.ts | 19 ++++++++++++++---- 3 files changed, 62 insertions(+), 6 deletions(-) diff --git a/src/agents/agent-command.ts b/src/agents/agent-command.ts index d9e57648577..af28a3d17dd 100644 --- a/src/agents/agent-command.ts +++ b/src/agents/agent-command.ts @@ -813,17 +813,27 @@ async function agentCommandInternal( catalog: catalogForThinking.length > 0 ? catalogForThinking : undefined, }); } - if (!isThinkingLevelSupported({ provider, model, level: resolvedThinkLevel })) { + const catalogForThinking = modelCatalog ?? allowedModelCatalog; + const thinkingCatalog = catalogForThinking.length > 0 ? catalogForThinking : undefined; + if ( + !isThinkingLevelSupported({ + provider, + model, + level: resolvedThinkLevel, + catalog: thinkingCatalog, + }) + ) { const explicitThink = Boolean(thinkOnce || thinkOverride); if (explicitThink) { throw new Error( - `Thinking level "${resolvedThinkLevel}" is not supported for ${provider}/${model}. Use one of: ${formatThinkingLevels(provider, model)}.`, + `Thinking level "${resolvedThinkLevel}" is not supported for ${provider}/${model}. Use one of: ${formatThinkingLevels(provider, model, ", ", thinkingCatalog)}.`, ); } const fallbackThinkLevel = resolveSupportedThinkingLevel({ provider, model, level: resolvedThinkLevel, + catalog: thinkingCatalog, }); if (fallbackThinkLevel !== resolvedThinkLevel) { const previousThinkLevel = resolvedThinkLevel; diff --git a/src/auto-reply/thinking.test.ts b/src/auto-reply/thinking.test.ts index 7ab99adf885..ed32e6de8e8 100644 --- a/src/auto-reply/thinking.test.ts +++ b/src/auto-reply/thinking.test.ts @@ -12,6 +12,8 @@ let listThinkingLevelOptions: typeof import("./thinking.js").listThinkingLevelOp let listThinkingLevels: typeof import("./thinking.js").listThinkingLevels; let normalizeReasoningLevel: typeof import("./thinking.js").normalizeReasoningLevel; let normalizeThinkLevel: typeof import("./thinking.js").normalizeThinkLevel; +let isThinkingLevelSupported: typeof import("./thinking.js").isThinkingLevelSupported; +let formatThinkingLevels: typeof import("./thinking.js").formatThinkingLevels; let resolveSupportedThinkingLevel: typeof import("./thinking.js").resolveSupportedThinkingLevel; let resolveThinkingDefaultForModel: typeof import("./thinking.js").resolveThinkingDefaultForModel; @@ -42,6 +44,8 @@ beforeEach(async () => { listThinkingLevels, normalizeReasoningLevel, normalizeThinkLevel, + isThinkingLevelSupported, + formatThinkingLevels, resolveSupportedThinkingLevel, resolveThinkingDefaultForModel, } = await loadFreshThinkingModuleForTest()); @@ -170,6 +174,37 @@ describe("listThinkingLevels", () => { expect(listThinkingLevelLabels("demo", "demo-model")).toEqual(["off", "on"]); }); + it("passes catalog reasoning into provider thinking profiles for support checks", () => { + providerRuntimeMocks.resolveProviderThinkingProfile.mockImplementation(({ context }) => ({ + levels: + context.reasoning === true + ? [{ id: "off" }, { id: "low" }, { id: "medium" }, { id: "high" }, { id: "max" }] + : [{ id: "off" }], + defaultLevel: "off", + })); + const catalog = [{ provider: "ollama", id: "gpt-oss:20b", name: "gpt-oss", reasoning: true }]; + + expect( + isThinkingLevelSupported({ + provider: "ollama", + model: "gpt-oss:20b", + level: "max", + catalog, + }), + ).toBe(true); + expect(formatThinkingLevels("ollama", "gpt-oss:20b", ", ", catalog)).toBe( + "off, low, medium, high, max", + ); + expect( + resolveSupportedThinkingLevel({ + provider: "ollama", + model: "gpt-oss:20b", + level: "max", + catalog, + }), + ).toBe("max"); + }); + it("maps stale unsupported levels to the largest profile level", () => { providerRuntimeMocks.resolveProviderThinkingProfile.mockReturnValue({ levels: [{ id: "off" }, { id: "high" }], diff --git a/src/auto-reply/thinking.ts b/src/auto-reply/thinking.ts index e0abe1bac52..7d1aec14951 100644 --- a/src/auto-reply/thinking.ts +++ b/src/auto-reply/thinking.ts @@ -194,8 +194,11 @@ function supportsThinkingLevel( provider: string | null | undefined, model: string | null | undefined, level: ThinkLevel, + catalog?: ThinkingCatalogEntry[], ): boolean { - return resolveThinkingProfile({ provider, model }).levels.some((entry) => entry.id === level); + return resolveThinkingProfile({ provider, model, catalog }).levels.some( + (entry) => entry.id === level, + ); } export function supportsXHighThinking(provider?: string | null, model?: string | null): boolean { @@ -223,8 +226,10 @@ export function formatThinkingLevels( provider?: string | null, model?: string | null, separator = ", ", + catalog?: ThinkingCatalogEntry[], ): string { - return listThinkingLevelLabels(provider, model).join(separator); + const profile = resolveThinkingProfile({ provider, model, catalog }); + return profile.levels.map(({ label }) => label).join(separator); } export function resolveThinkingDefaultForModel(params: { @@ -262,8 +267,9 @@ export function isThinkingLevelSupported(params: { provider?: string | null; model?: string | null; level: ThinkLevel; + catalog?: ThinkingCatalogEntry[]; }): boolean { - return supportsThinkingLevel(params.provider, params.model, params.level); + return supportsThinkingLevel(params.provider, params.model, params.level, params.catalog); } function resolveSupportedThinkingLevelFromProfile( @@ -286,7 +292,12 @@ export function resolveSupportedThinkingLevel(params: { provider?: string | null; model?: string | null; level: ThinkLevel; + catalog?: ThinkingCatalogEntry[]; }): ThinkLevel { - const profile = resolveThinkingProfile({ provider: params.provider, model: params.model }); + const profile = resolveThinkingProfile({ + provider: params.provider, + model: params.model, + catalog: params.catalog, + }); return resolveSupportedThinkingLevelFromProfile(profile, params.level); }