feat: support per-model thinkingDefault override in models config#18152
feat: support per-model thinkingDefault override in models config#18152steipete merged 1 commit intoopenclaw:mainfrom
Conversation
src/agents/model-selection.ts
Outdated
| const modelKey = `${params.provider}/${params.model}`; | ||
| const perModelEntry = params.cfg.agents?.defaults?.models?.[modelKey]; | ||
| const perModelThinking = perModelEntry?.thinkingDefault as ThinkLevel | undefined; | ||
| if (perModelThinking) { | ||
| return perModelThinking; | ||
| } |
There was a problem hiding this comment.
Per-model lookup skips key normalization
The config key lookup here uses a raw provider/model string from the (already-normalized) caller, but the keys in cfg.agents.defaults.models are user-provided raw strings that may not be in normalized form. Other functions in this file (e.g., buildConfiguredAllowlistKeys at line 140, buildAllowedModelSet at line 311, buildModelAliasIndex at line 157) all normalize config keys via parseModelRef before matching.
If a user writes a non-canonical alias as a config key — e.g., "anthropic/opus-4.6" (normalized to "anthropic/claude-opus-4-6"), "google/gemini-3-pro" (normalized to "google/gemini-3-pro-preview"), or "qwen/some-model" (provider normalizes to "qwen-portal") — the direct dictionary lookup will miss the entry, silently falling through to the global default.
Consider iterating over config model entries and normalizing keys (consistent with other lookups in this file):
const models = params.cfg.agents?.defaults?.models ?? {};
for (const [rawKey, entry] of Object.entries(models)) {
const parsed = parseModelRef(rawKey, params.provider);
if (parsed && parsed.provider === params.provider && parsed.model === params.model && entry?.thinkingDefault) {
return entry.thinkingDefault as ThinkLevel;
}
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/model-selection.ts
Line: 429:434
Comment:
**Per-model lookup skips key normalization**
The config key lookup here uses a raw `provider/model` string from the (already-normalized) caller, but the keys in `cfg.agents.defaults.models` are user-provided raw strings that may not be in normalized form. Other functions in this file (e.g., `buildConfiguredAllowlistKeys` at line 140, `buildAllowedModelSet` at line 311, `buildModelAliasIndex` at line 157) all normalize config keys via `parseModelRef` before matching.
If a user writes a non-canonical alias as a config key — e.g., `"anthropic/opus-4.6"` (normalized to `"anthropic/claude-opus-4-6"`), `"google/gemini-3-pro"` (normalized to `"google/gemini-3-pro-preview"`), or `"qwen/some-model"` (provider normalizes to `"qwen-portal"`) — the direct dictionary lookup will miss the entry, silently falling through to the global default.
Consider iterating over config model entries and normalizing keys (consistent with other lookups in this file):
```
const models = params.cfg.agents?.defaults?.models ?? {};
for (const [rawKey, entry] of Object.entries(models)) {
const parsed = parseModelRef(rawKey, params.provider);
if (parsed && parsed.provider === params.provider && parsed.model === params.model && entry?.thinkingDefault) {
return entry.thinkingDefault as ThinkLevel;
}
}
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Good catch — fixed in 1cfb89d. The per-model lookup now iterates over config entries and normalizes keys via parseModelRef, consistent with buildModelAliasIndex, buildAllowedModelSet, and other lookups in this file. Aliases like "anthropic/opus-4.6" will now resolve correctly instead of silently falling through.
1cfb89d to
e893f16
Compare
The global `agents.defaults.thinkingDefault` forces a single thinking
level for all models. Users running multiple models with different
reasoning capabilities (e.g. Claude with extended thinking, GPT-4o
without, Gemini Flash with lightweight reasoning) cannot optimise the
thinking level per model.
Add an optional `thinkingDefault` field to `AgentModelEntryConfig` so
each entry under `agents.defaults.models` can declare its own default.
Resolution priority: per-model → global → catalog auto-detect.
Example config:
"models": {
"anthropic/claude-sonnet-4-20250514": { "thinkingDefault": "high" },
"openai/gpt-4o": { "thinkingDefault": "off" }
}
Co-authored-by: Cursor <[email protected]>
e893f16 to
8b88d51
Compare
|
Reverted in b846dc0. This was an accidental merge, so the changes have been reverted. |
|
Hi @sebslight, thanks for the heads-up. I noticed this was reverted alongside several other PRs from the same day. Is there anything specific about the feature itself that needs rework, or was this primarily a process-related revert? Happy to re-submit with any changes needed once the timing is right. |
|
Hey @wu-tian807 , I'm building a multi-agent orchestration system on top of OpenClaw where an Orchestrator agent (running MiniMax-M2.5) decomposes tasks and spawns sub-agents (Coder, Reviewer, etc.). The ORC needs thinking: high for strategic decomposition, while the Coder agents work fine with low or off. Currently I'm using --thinking high as a CLI flag when spawning the ORC via openclaw agent, which works but means the thinking level lives in my worker code rather than in config. A per-model thinkingDefault would be much cleaner — I could set it once in config and have it resolve automatically based on which model is active. Would love to see this re-landed. |
Add per-model thinkingDefault to resolve thinking level based on which model is active, so users running different models (e.g. o3 for chat, gemini-flash for heartbeats) get appropriate thinking levels automatically without manually toggling /think per session. Resolution priority: per-model → global → catalog auto-detect (existing fallback behavior unchanged). Config keys are normalized via parseModelRef so aliases like "anthropic/opus-4.6" resolve correctly. Also adds: - /think default directive to undo a session-level thinking override and cascade back to the per-model default - Re-resolve thinking default after inline /model switch - Memory flush passes provider/model through for correct resolution Related: openclaw#18152 (built independently, shares the per-model config concept but adds /think default and model-switch re-resolution AI-assisted (Claude Code). Tested: pnpm build, pnpm check, pnpm test. Manually verified /think default resets level and per-model config resolves correctly via openclaw doctor. EOF )
Add per-model thinkingDefault to resolve thinking level based on which model is active, so users running different models (e.g. o3 for chat, gemini-flash for heartbeats) get appropriate thinking levels automatically without manually toggling /think per session. Resolution priority: per-model → global → catalog auto-detect (existing fallback behavior unchanged). Config keys are normalized via parseModelRef so aliases like "anthropic/opus-4.6" resolve correctly. Also adds: - /think default directive to undo a session-level thinking override and cascade back to the per-model default - Re-resolve thinking default after inline /model switch - Memory flush passes provider/model through for correct resolution Related: openclaw#18152 (built independently, shares the per-model config concept but adds /think default and model-switch re-resolution AI-assisted (Claude Code). Tested: pnpm build, pnpm check, pnpm test. Manually verified /think default resets level and per-model config resolves correctly via openclaw doctor. EOF )
Add per-model thinkingDefault to resolve thinking level based on which model is active, so users running different models (e.g. o3 for chat, gemini-flash for heartbeats) get appropriate thinking levels automatically without manually toggling /think per session. Resolution priority: per-model → global → catalog auto-detect (existing fallback behavior unchanged). Config keys are normalized via parseModelRef so aliases like "anthropic/opus-4.6" resolve correctly. Also adds: - /think default directive to undo a session-level thinking override and cascade back to the per-model default - Re-resolve thinking default after inline /model switch - Memory flush passes provider/model through for correct resolution Related: openclaw#18152 (built independently, shares the per-model config concept but adds /think default and model-switch re-resolution AI-assisted (Claude Code). Tested: pnpm build, pnpm check, pnpm test. Manually verified /think default resets level and per-model config resolves correctly via openclaw doctor. EOF )
|
I created this feature independently about 3 days ago and noticed this related change during a rebase today. Decided to push it as #20458 which addresses the same per-model
|
Add per-model thinkingDefault to resolve thinking level based on which model is active, so users running different models (e.g. gemini-3-pro for chat, gemini-3-flash for heartbeats) get appropriate thinking levels automatically without manually toggling /think per session. Resolution priority: per-model → global → catalog auto-detect (existing fallback behavior unchanged). Config keys are normalized via parseModelRef so aliases like "anthropic/opus-4.6" resolve correctly. Also adds: - /think default directive to undo a session-level thinking override and cascade back to the per-model default - Re-resolve thinking default after inline /model switch for both directive-only and inline-with-content paths - Memory flush passes provider/model through for correct resolution Related: openclaw#18152 (built independently, shares the per-model config concept but adds /think default and model-switch re-resolution) AI-assisted (Claude Code), fully tested. Tested: pnpm build, pnpm check, pnpm test, and e2e verified on live gateway.
|
Thanks @holgergruenhagen for sharing your use case — great to see this would directly help your multi-agent orchestration setup! @kmixter Your #20458 looks like the right path forward — the |
Summary
With the recent addition of separate heartbeat model overrides (
heartbeat.model), users now commonly run different models for heartbeat vs. chat — e.g., a lightweight model likegemini/gemini-2.5-flashfor periodic heartbeats and a heavier model likeopenai/o3for interactive conversations.The problem: these models have very different reasoning capabilities, but
thinkingDefaultis a single global setting. Setting"high"works great for O3 but wastes tokens (or errors) on Gemini Flash; setting"off"globally means O3 never uses its extended thinking. Today the only workaround is manually toggling/thinkper session, which defeats the purpose of autonomous heartbeat runs.This PR adds an optional
thinkingDefaultfield to each model entry underagents.defaults.models, so the thinking level resolves automatically based on which model is active.Resolution priority: per-model → global → catalog auto-detect (existing fallback behavior unchanged).
Config example
{ "agents": { "defaults": { "thinkingDefault": "low", "models": { "anthropic/claude-opus-4-6": { "thinkingDefault": "high" }, "google/gemini-2.5-flash": { "thinkingDefault": "low" }, "openai/gpt-4o": { "thinkingDefault": "off" } } } } }Changes (3 files, +32 lines)
src/config/types.agent-defaults.ts— Add optionalthinkingDefaulttoAgentModelEntryConfigsrc/config/zod-schema.agent-defaults.ts— Add zod validation for the new fieldsrc/agents/model-selection.ts— Look up per-model entry before falling back to global default inresolveThinkingDefault()Backward compatible
thinkingDefaultbehave exactly as beforeTest plan
pnpm buildpassestsgo --noEmittype check passesoxlinton changed files: 0 errorsmodel-selection.test.ts(10 tests) passesrun.skill-filter.test.ts(5 tests, imports resolveThinkingDefault) passesconfig.schema-regressions.test.tspasses