Skip to content

feat: support per-model thinkingDefault override in models config#18152

Merged
steipete merged 1 commit intoopenclaw:mainfrom
wu-tian807:feat/per-model-thinking-default
Feb 16, 2026
Merged

feat: support per-model thinkingDefault override in models config#18152
steipete merged 1 commit intoopenclaw:mainfrom
wu-tian807:feat/per-model-thinking-default

Conversation

@wu-tian807
Copy link
Contributor

@wu-tian807 wu-tian807 commented Feb 16, 2026

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 like gemini/gemini-2.5-flash for periodic heartbeats and a heavier model like openai/o3 for interactive conversations.

The problem: these models have very different reasoning capabilities, but thinkingDefault is 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 /think per session, which defeats the purpose of autonomous heartbeat runs.

This PR adds an optional thinkingDefault field to each model entry under agents.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 optional thinkingDefault to AgentModelEntryConfig
  • src/config/zod-schema.agent-defaults.ts — Add zod validation for the new field
  • src/agents/model-selection.ts — Look up per-model entry before falling back to global default in resolveThinkingDefault()

Backward compatible

  • New field is fully optional
  • Existing configs without per-model thinkingDefault behave exactly as before
  • No breaking changes to the resolution chain

Test plan

  • pnpm build passes
  • tsgo --noEmit type check passes
  • oxlint on changed files: 0 errors
  • model-selection.test.ts (10 tests) passes
  • run.skill-filter.test.ts (5 tests, imports resolveThinkingDefault) passes
  • config.schema-regressions.test.ts passes

@openclaw-barnacle openclaw-barnacle bot added agents Agent runtime and tooling size: XS labels Feb 16, 2026
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines 429 to 434
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;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@wu-tian807 wu-tian807 force-pushed the feat/per-model-thinking-default branch 2 times, most recently from 1cfb89d to e893f16 Compare February 16, 2026 15:22
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]>
@wu-tian807 wu-tian807 force-pushed the feat/per-model-thinking-default branch from e893f16 to 8b88d51 Compare February 16, 2026 15:28
@steipete steipete merged commit 671f913 into openclaw:main Feb 16, 2026
23 checks passed
@sebslight
Copy link
Member

Reverted in b846dc0.

This was an accidental merge, so the changes have been reverted.

@wu-tian807
Copy link
Contributor Author

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.

@holgergruenhagen
Copy link

Hey @wu-tian807 ,
I just wanted to chime in as someone who'd benefit from this feature.

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.
Happy to test if there's a new iteration. 👍

kmixter added a commit to kmixter/openclaw that referenced this pull request Feb 18, 2026
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
)
kmixter added a commit to kmixter/openclaw that referenced this pull request Feb 18, 2026
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
)
kmixter added a commit to kmixter/openclaw that referenced this pull request Feb 19, 2026
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
)
@kmixter
Copy link

kmixter commented Feb 19, 2026

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 thinkingDefault config resolution (with key normalization via parseModelRef) but also adds:

  • /think default — undoes a session-level /think override so thinking cascades back to the per-model default
  • Per-model re-resolution on model switch — works for both directive-only (/model openai/gpt-4) and inline with content (/model openai/gpt-4 what is 2+2?) paths

kmixter added a commit to kmixter/openclaw that referenced this pull request Feb 19, 2026
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.
@wu-tian807
Copy link
Contributor Author

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 /think default directive and model-switch re-resolution are solid additions on top of the core per-model config. Happy to defer to your more complete implementation. Let me know if I can help review or test anything.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling size: XS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

Comments