fix(agents): validate thinking with model catalog

This commit is contained in:
Peter Steinberger
2026-04-26 23:16:00 +01:00
parent 3746e5b969
commit ff9fefb79b
3 changed files with 62 additions and 6 deletions

View File

@@ -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;

View File

@@ -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" }],

View File

@@ -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);
}