mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:50:43 +00:00
fix(thinking): default implicit reasoning models to medium (#70601)
* fix(thinking): default implicit reasoning models to medium * fix(thinking): preserve reasoning metadata during default resolution
This commit is contained in:
@@ -42,6 +42,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Thinking defaults/status: raise the implicit default thinking level for reasoning-capable models from legacy `off`/`low` fallback behavior to a safe provider-supported `medium` equivalent when no explicit config default is set, preserve configured-model reasoning metadata when runtime catalog loading is empty, and make `/status` report the same resolved default as runtime.
|
||||
- Gateway/model pricing: fetch OpenRouter and LiteLLM pricing asynchronously at startup and extend catalog fetch timeouts to 30 seconds, reducing noisy timeout warnings during slow upstream responses.
|
||||
- Status: show `Fast` in `/status` when fast mode is enabled, including config/default-derived fast mode, and omit it when disabled.
|
||||
- OpenAI/image generation: detect Azure OpenAI-style image endpoints, use Azure `api-key` auth plus deployment-scoped image URLs, and honor `AZURE_OPENAI_API_VERSION` so image generation and edits work against Azure-hosted OpenAI resources. (#70570) Thanks @zhanggpcsu.
|
||||
|
||||
@@ -1363,10 +1363,10 @@ describe("model-selection", () => {
|
||||
expect(resolveAnthropicOpus47Thinking(cfg)).toBe("off");
|
||||
});
|
||||
|
||||
it("falls back to low when no provider thinking hook is active", () => {
|
||||
it("falls back to medium when no provider thinking hook is active", () => {
|
||||
const cfg = {} as OpenClawConfig;
|
||||
|
||||
expect(resolveAnthropicOpusThinking(cfg)).toBe("low");
|
||||
expect(resolveAnthropicOpusThinking(cfg)).toBe("medium");
|
||||
|
||||
expect(
|
||||
resolveThinkingDefault({
|
||||
@@ -1382,7 +1382,7 @@ describe("model-selection", () => {
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toBe("low");
|
||||
).toBe("medium");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { resolveThinkingDefaultForModel } from "../auto-reply/thinking.shared.js";
|
||||
import { resolveThinkingDefaultForModel } from "../auto-reply/thinking.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
|
||||
@@ -141,6 +141,7 @@ function createModelCatalogModuleMock() {
|
||||
provider: "openai",
|
||||
id: "gpt-5.4",
|
||||
name: "GPT-5.4",
|
||||
reasoning: true,
|
||||
contextWindow: 400000,
|
||||
},
|
||||
],
|
||||
@@ -940,6 +941,104 @@ describe("session_status tool", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("uses the implicit model thinking default when no config default is set", async () => {
|
||||
resetSessionStore({
|
||||
"agent:kira:main": {
|
||||
sessionId: "agent-thinking-implicit",
|
||||
updatedAt: 10,
|
||||
},
|
||||
});
|
||||
const savedConfig = mockConfig;
|
||||
try {
|
||||
mockConfig = {
|
||||
session: { mainKey: "main", scope: "per-sender" },
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openai/gpt-5.4" },
|
||||
models: {},
|
||||
},
|
||||
list: [
|
||||
{
|
||||
id: "kira",
|
||||
model: "openai/gpt-5.4",
|
||||
},
|
||||
],
|
||||
},
|
||||
tools: {
|
||||
agentToAgent: { enabled: false },
|
||||
},
|
||||
};
|
||||
|
||||
const tool = getSessionStatusTool("agent:kira:main");
|
||||
|
||||
await tool.execute("call-agent-thinking-implicit", {});
|
||||
|
||||
expect(buildStatusMessageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agentId: "kira",
|
||||
agent: expect.objectContaining({
|
||||
thinkingDefault: "medium",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
mockConfig = savedConfig;
|
||||
}
|
||||
});
|
||||
|
||||
it("hydrates runtime catalog metadata for status when configured model metadata omits reasoning", async () => {
|
||||
resetSessionStore({
|
||||
"agent:kira:main": {
|
||||
sessionId: "agent-thinking-runtime-hydration",
|
||||
updatedAt: 10,
|
||||
},
|
||||
});
|
||||
const savedConfig = mockConfig;
|
||||
try {
|
||||
mockConfig = {
|
||||
session: { mainKey: "main", scope: "per-sender" },
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openai/gpt-5.4" },
|
||||
models: {},
|
||||
},
|
||||
list: [
|
||||
{
|
||||
id: "kira",
|
||||
model: "openai/gpt-5.4",
|
||||
},
|
||||
],
|
||||
},
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
models: [{ id: "gpt-5.4", name: "GPT-5.4" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
agentToAgent: { enabled: false },
|
||||
},
|
||||
};
|
||||
|
||||
const tool = getSessionStatusTool("agent:kira:main");
|
||||
|
||||
await tool.execute("call-agent-thinking-runtime-hydration", {});
|
||||
|
||||
expect(buildStatusMessageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agentId: "kira",
|
||||
agent: expect.objectContaining({
|
||||
thinkingDefault: "medium",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
mockConfig = savedConfig;
|
||||
}
|
||||
});
|
||||
|
||||
it("falls back to origin.provider when resolving queue settings", async () => {
|
||||
resetSessionStore({
|
||||
main: {
|
||||
|
||||
@@ -28,10 +28,12 @@ import { formatTaskStatusDetail, formatTaskStatusTitle } from "../../tasks/task-
|
||||
import { loadModelCatalog } from "../model-catalog.js";
|
||||
import {
|
||||
buildAllowedModelSet,
|
||||
buildConfiguredModelCatalog,
|
||||
buildModelAliasIndex,
|
||||
modelKey,
|
||||
resolveDefaultModelForAgent,
|
||||
resolveModelRefFromString,
|
||||
resolveThinkingDefault,
|
||||
} from "../model-selection.js";
|
||||
import {
|
||||
describeSessionStatusTool,
|
||||
@@ -561,7 +563,32 @@ export function createSessionStatusTool(opts?: {
|
||||
resolvedVerboseLevel: (statusSessionEntry.verboseLevel ?? "off") as VerboseLevel,
|
||||
resolvedReasoningLevel: (statusSessionEntry.reasoningLevel ?? "off") as ReasoningLevel,
|
||||
resolvedElevatedLevel: statusSessionEntry.elevatedLevel as ElevatedLevel | undefined,
|
||||
resolveDefaultThinkingLevel: async () => cfg.agents?.defaults?.thinkingDefault,
|
||||
resolveDefaultThinkingLevel: async () => {
|
||||
const configuredCatalog = buildConfiguredModelCatalog({ cfg });
|
||||
const configuredSelectedEntry = configuredCatalog.find(
|
||||
(entry) => entry.provider === providerForCard && entry.id === defaultModelForCard,
|
||||
);
|
||||
const shouldHydrateRuntimeCatalog =
|
||||
configuredCatalog.length === 0 ||
|
||||
!configuredSelectedEntry ||
|
||||
configuredSelectedEntry.reasoning === undefined;
|
||||
const runtimeCatalog = shouldHydrateRuntimeCatalog
|
||||
? await loadModelCatalog({ config: cfg })
|
||||
: undefined;
|
||||
const runtimeSelectedEntry = runtimeCatalog?.find(
|
||||
(entry) => entry.provider === providerForCard && entry.id === defaultModelForCard,
|
||||
);
|
||||
const catalog =
|
||||
runtimeSelectedEntry || configuredCatalog.length === 0
|
||||
? (runtimeCatalog ?? configuredCatalog)
|
||||
: configuredCatalog;
|
||||
return resolveThinkingDefault({
|
||||
cfg,
|
||||
provider: providerForCard,
|
||||
model: defaultModelForCard,
|
||||
catalog,
|
||||
});
|
||||
},
|
||||
isGroup,
|
||||
defaultGroupActivation: () => "mention",
|
||||
taskLineOverride: taskLine,
|
||||
|
||||
@@ -75,6 +75,77 @@ describe("createModelSelectionState catalog loading", () => {
|
||||
expect(loadModelCatalog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses the implicit model default when no global thinking default is configured", async () => {
|
||||
vi.mocked(loadModelCatalog).mockClear();
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"openai-codex/gpt-5.4": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
models: {
|
||||
providers: {
|
||||
"openai-codex": {
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
models: [makeConfiguredModel()],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const state = await createModelSelectionState({
|
||||
cfg,
|
||||
agentCfg: cfg.agents?.defaults,
|
||||
defaultProvider: "openai-codex",
|
||||
defaultModel: "gpt-5.4",
|
||||
provider: "openai-codex",
|
||||
model: "gpt-5.4",
|
||||
hasModelDirective: false,
|
||||
});
|
||||
|
||||
await expect(state.resolveDefaultThinkingLevel()).resolves.toBe("medium");
|
||||
expect(loadModelCatalog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("hydrates runtime catalog metadata when the configured allowlist entry lacks reasoning", async () => {
|
||||
vi.mocked(loadModelCatalog).mockClear();
|
||||
vi.mocked(loadModelCatalog).mockResolvedValueOnce([
|
||||
{ provider: "openai-codex", id: "gpt-5.4", name: "GPT-5.4", reasoning: true },
|
||||
]);
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"openai-codex/gpt-5.4": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
models: {
|
||||
providers: {
|
||||
"openai-codex": {
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
models: [makeConfiguredModel({ reasoning: undefined })],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const state = await createModelSelectionState({
|
||||
cfg,
|
||||
agentCfg: cfg.agents?.defaults,
|
||||
defaultProvider: "openai-codex",
|
||||
defaultModel: "gpt-5.4",
|
||||
provider: "openai-codex",
|
||||
model: "gpt-5.4",
|
||||
hasModelDirective: false,
|
||||
});
|
||||
|
||||
await expect(state.resolveDefaultThinkingLevel()).resolves.toBe("medium");
|
||||
expect(loadModelCatalog).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("prefers per-agent thinkingDefault over model and global defaults", async () => {
|
||||
vi.mocked(loadModelCatalog).mockClear();
|
||||
const cfg = {
|
||||
|
||||
@@ -457,11 +457,26 @@ export async function createModelSelectionState(params: {
|
||||
defaultThinkingLevel = explicitThinkingDefault;
|
||||
return defaultThinkingLevel;
|
||||
}
|
||||
if (!modelCatalog) {
|
||||
let catalogForThinking =
|
||||
modelCatalog && modelCatalog.length > 0 ? modelCatalog : allowedModelCatalog;
|
||||
const selectedCatalogEntry = catalogForThinking?.find(
|
||||
(entry) => entry.provider === provider && entry.id === model,
|
||||
);
|
||||
const shouldHydrateRuntimeCatalog =
|
||||
!modelCatalog && (!selectedCatalogEntry || selectedCatalogEntry.reasoning === undefined);
|
||||
if (shouldHydrateRuntimeCatalog) {
|
||||
modelCatalog = await (await loadModelCatalogRuntime()).loadModelCatalog({ config: cfg });
|
||||
logStage("catalog-loaded-for-thinking", `entries=${modelCatalog.length}`);
|
||||
const runtimeSelectedEntry = modelCatalog.find(
|
||||
(entry) => entry.provider === provider && entry.id === model,
|
||||
);
|
||||
catalogForThinking =
|
||||
runtimeSelectedEntry || !catalogForThinking || catalogForThinking.length === 0
|
||||
? modelCatalog.length > 0
|
||||
? modelCatalog
|
||||
: allowedModelCatalog
|
||||
: allowedModelCatalog;
|
||||
}
|
||||
const catalogForThinking = modelCatalog.length > 0 ? modelCatalog : allowedModelCatalog;
|
||||
const resolved = resolveThinkingDefault({
|
||||
cfg,
|
||||
provider,
|
||||
|
||||
@@ -259,16 +259,49 @@ describe("resolveThinkingDefaultForModel", () => {
|
||||
).toBe("off");
|
||||
});
|
||||
|
||||
it("defaults reasoning-capable catalog models to low", () => {
|
||||
it("defaults reasoning-capable catalog models to medium", () => {
|
||||
expect(
|
||||
resolveThinkingDefaultForModel({
|
||||
provider: "openai",
|
||||
model: "gpt-5.4",
|
||||
catalog: [{ provider: "openai", id: "gpt-5.4", reasoning: true }],
|
||||
}),
|
||||
).toBe("medium");
|
||||
});
|
||||
|
||||
it("remaps implicit reasoning defaults to the strongest supported level at or below medium", () => {
|
||||
providerRuntimeMocks.resolveProviderBinaryThinking.mockImplementation(
|
||||
({ provider }) => provider === "demo-binary",
|
||||
);
|
||||
|
||||
expect(
|
||||
resolveThinkingDefaultForModel({
|
||||
provider: "demo-binary",
|
||||
model: "demo-model",
|
||||
catalog: [{ provider: "demo-binary", id: "demo-model", reasoning: true }],
|
||||
}),
|
||||
).toBe("low");
|
||||
});
|
||||
|
||||
it("keeps catalog reasoning context when remapping implicit reasoning defaults", () => {
|
||||
providerRuntimeMocks.resolveProviderThinkingProfile.mockImplementation(
|
||||
({ provider, context }) =>
|
||||
provider === "demo-contextual" && context.reasoning
|
||||
? { levels: [{ id: "off" }, { id: "low" }, { id: "medium" }] }
|
||||
: provider === "demo-contextual"
|
||||
? { levels: [{ id: "off" }] }
|
||||
: undefined,
|
||||
);
|
||||
|
||||
expect(
|
||||
resolveThinkingDefaultForModel({
|
||||
provider: "demo-contextual",
|
||||
model: "demo-model",
|
||||
catalog: [{ provider: "demo-contextual", id: "demo-model", reasoning: true }],
|
||||
}),
|
||||
).toBe("medium");
|
||||
});
|
||||
|
||||
it("defaults to off when no adaptive or reasoning hint is present", () => {
|
||||
expect(
|
||||
resolveThinkingDefaultForModel({
|
||||
|
||||
@@ -230,7 +230,11 @@ export function resolveThinkingDefaultForModel(params: {
|
||||
if (profile.defaultLevel) {
|
||||
return profile.defaultLevel;
|
||||
}
|
||||
return resolveThinkingDefaultForModelFallback(params);
|
||||
const fallback = resolveThinkingDefaultForModelFallback(params);
|
||||
if (fallback === "off") {
|
||||
return "off";
|
||||
}
|
||||
return resolveSupportedThinkingLevelFromProfile(profile, "medium");
|
||||
}
|
||||
|
||||
export function resolveLargestSupportedThinkingLevel(
|
||||
@@ -252,20 +256,27 @@ export function isThinkingLevelSupported(params: {
|
||||
return supportsThinkingLevel(params.provider, params.model, params.level);
|
||||
}
|
||||
|
||||
function resolveSupportedThinkingLevelFromProfile(
|
||||
profile: ResolvedThinkingProfile,
|
||||
level: ThinkLevel,
|
||||
): ThinkLevel {
|
||||
if (profile.levels.some((entry) => entry.id === level)) {
|
||||
return level;
|
||||
}
|
||||
const requestedRank = THINKING_LEVEL_RANKS[level];
|
||||
const ranked = profile.levels.toSorted((a, b) => b.rank - a.rank);
|
||||
return (
|
||||
ranked.find((entry) => entry.id !== "off" && entry.rank <= requestedRank)?.id ??
|
||||
ranked.find((entry) => entry.id !== "off")?.id ??
|
||||
"off"
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveSupportedThinkingLevel(params: {
|
||||
provider?: string | null;
|
||||
model?: string | null;
|
||||
level: ThinkLevel;
|
||||
}): ThinkLevel {
|
||||
const profile = resolveThinkingProfile({ provider: params.provider, model: params.model });
|
||||
if (profile.levels.some((entry) => entry.id === params.level)) {
|
||||
return params.level;
|
||||
}
|
||||
const requestedRank = THINKING_LEVEL_RANKS[params.level];
|
||||
const ranked = profile.levels.toSorted((a, b) => b.rank - a.rank);
|
||||
return (
|
||||
ranked.find((level) => level.id !== "off" && level.rank <= requestedRank)?.id ??
|
||||
ranked.find((level) => level.id !== "off")?.id ??
|
||||
"off"
|
||||
);
|
||||
return resolveSupportedThinkingLevelFromProfile(profile, params.level);
|
||||
}
|
||||
|
||||
@@ -288,6 +288,9 @@ export async function buildStatusText(params: BuildStatusTextParams): Promise<st
|
||||
}).enabled;
|
||||
const agentFallbacksOverride = resolveAgentModelFallbacksOverride(cfg, statusAgentId);
|
||||
const { buildStatusMessage } = await loadStatusMessageRuntime();
|
||||
const explicitThinkingDefault =
|
||||
(agentConfig?.thinkingDefault as ThinkLevel | undefined) ??
|
||||
(agentDefaults.thinkingDefault as ThinkLevel | undefined);
|
||||
return buildStatusMessage({
|
||||
config: cfg,
|
||||
agent: {
|
||||
@@ -298,7 +301,7 @@ export async function buildStatusText(params: BuildStatusTextParams): Promise<st
|
||||
...(agentFallbacksOverride === undefined ? {} : { fallbacks: agentFallbacksOverride }),
|
||||
},
|
||||
...(typeof contextTokens === "number" && contextTokens > 0 ? { contextTokens } : {}),
|
||||
thinkingDefault: agentConfig?.thinkingDefault ?? agentDefaults.thinkingDefault,
|
||||
thinkingDefault: explicitThinkingDefault,
|
||||
verboseDefault: agentDefaults.verboseDefault,
|
||||
elevatedDefault: agentDefaults.elevatedDefault,
|
||||
},
|
||||
@@ -313,7 +316,8 @@ export async function buildStatusText(params: BuildStatusTextParams): Promise<st
|
||||
sessionScope,
|
||||
sessionStorePath: storePath,
|
||||
groupActivation,
|
||||
resolvedThink: resolvedThinkLevel ?? (await resolveDefaultThinkingLevel()),
|
||||
resolvedThink:
|
||||
resolvedThinkLevel ?? explicitThinkingDefault ?? (await resolveDefaultThinkingLevel()),
|
||||
resolvedFast: effectiveFastMode,
|
||||
resolvedVerbose: resolvedVerboseLevel,
|
||||
resolvedReasoning: resolvedReasoningLevel,
|
||||
|
||||
Reference in New Issue
Block a user