Skip to content

feat: runtime model switching via /model command#160

Open
Talla wants to merge 2 commits intoRichardAtCT:mainfrom
Talla:feat/model-command
Open

feat: runtime model switching via /model command#160
Talla wants to merge 2 commits intoRichardAtCT:mainfrom
Talla:feat/model-command

Conversation

@Talla
Copy link
Copy Markdown

@Talla Talla commented Mar 20, 2026

Summary

Adds a /model command that lets users switch between Claude models (Opus, Sonnet, Haiku) and set effort levels at runtime via an inline keyboard UI — no bot restart or config changes needed.

  • Inline keyboard flow: /model → pick model → pick effort level (if supported) → done
  • Model-aware effort levels: Haiku has no effort support, Sonnet supports low/medium/high, Opus adds "max"
  • Session handling: Switching models forces a new session (CLI doesn't support model changes on resumed sessions). Users are warned upfront before selecting.
  • Per-user override: Stored in context.user_data (in-memory). Resets on bot restart — server default (CLAUDE_MODEL env var) is used until the user overrides.

Implements the feature requested in #138.

Changes

File What
command.py /model command, model_callback, _handle_model_selection shared logic, model/effort constants
callback.py Route model: and effort: callbacks in classic mode
orchestrator.py Register command + callback in both modes, add to bot menu, thread overrides to run_command
message.py Thread model_override + effort_override through all 4 classic run_command call sites
facade.py Accept and forward model_override + effort_override params
sdk_integration.py Apply model_override and effort to ClaudeAgentOptions
test_model_command.py 17 new tests (keyboard, selection, labels, effort config)
test_orchestrator.py Updated handler count assertions

Test plan

  • All 529 tests pass (512 existing + 17 new)
  • Manual: /model shows keyboard with Opus/Sonnet/Haiku + Reset
  • Manual: Selecting Haiku skips effort selection
  • Manual: Selecting Sonnet shows Low/Medium/High (no Max)
  • Manual: Selecting Opus shows Low/Medium/High/Max
  • Manual: After switching, next message uses the new model
  • Manual: "Reset to default" clears override

Closes #138

🤖 Generated with Claude Code

Talla added 2 commits March 20, 2026 18:25
Add /model command with inline keyboard UI for switching between
Opus/Sonnet/Haiku models and effort levels (low/medium/high/max)
at runtime. Model changes force a new session since the CLI doesn't
support model switching on resumed sessions.

- Effort levels are model-aware: Haiku has none, Sonnet excludes
  "max", Opus supports all including "max"
- Override is per-user via context.user_data (in-memory, resets on
  bot restart)
- Threaded through all run_command call sites (orchestrator, classic
  message handler) into the SDK layer
- Registered in both agentic and classic handler modes
- Added to bot command menu and /help text
- 17 new tests covering keyboard display, model/effort selection,
  label formatting, and effort-per-model configuration

Closes RichardAtCT#138
Instead of just "Default", show "Default (claude-sonnet-4-6)" or
"Default (CLI default)" so users can verify what model is active
after resetting.
@RichardAtCT
Copy link
Copy Markdown
Owner

Good implementation overall — the UX flow is clean and the test coverage is solid. Six issues to address before merging:


🔴 Bug: action closure in _model_effort_handler (callback.py)

This is the most critical issue. _model_effort_handler references action from the outer scope, but action is a loop variable (or however handlers is constructed). Both "model" and "effort" entries point to the same closure, and action will be whatever value it held last when the dict was defined — likely "effort" for both handlers. This means a model:opus callback will be reconstructed as effort:opus and silently fail.

Fix by capturing explicitly:

async def _make_model_effort_handler(action: str):
    async def handler(query, param, context) -> None:
        await _handle_model_selection(query, f"{action}:{param}", context)
    return handler

Or just inline two named functions — cleaner and avoids the factory pattern for async.


🔴 Missing type annotations (mypy strict)

_handle_model_selection is missing return type and parameter annotations. With disallow_untyped_defs=true this will fail CI. Should be:

async def _handle_model_selection(
    query: CallbackQuery,
    data: str,
    context: ContextTypes.DEFAULT_TYPE,
) -> None:

Same applies to model_commandupdate and context need annotations.


🟡 effort_override not visibly applied in sdk_integration.py

The diff shows model_override wired into ClaudeAgentOptions but effort_override is elided. If it's genuinely there in the full diff, fine — but if not, this is a silent no-op: users select an effort level, get confirmation text, but the SDK ignores it. Needs explicit confirmation.


🟡 Hardcoded model IDs will go stale

"haiku": "claude-haiku-4-5-20251001" is already date-versioned and will need updating as Anthropic releases new versions. Consider moving _MODELS to config/env so it's overridable without a deploy, or at minimum add a comment linking to the Anthropic model docs.


🟡 user_data vs chat_data — potential conflict with PR #165

PR #165 migrates session state to chat_data. This PR stores model/effort overrides in user_data. In a group chat, one user's model choice would only affect their user_data while the session (in chat_data) is shared — meaning the model applied depends on who sent the last message. These two PRs need to be reconciled before either merges.


🟢 Test quality — mostly good

17 tests covering happy path, reset, Haiku (no effort), and effort selection is solid. Two suggestions:

  • Add a test for the action closure bug (would have caught it)
  • Add a test asserting force_new_session is set on model change

Summary: The action closure bug is a functional regression that will silently swallow model selections. The mypy annotations are a CI blocker under strict mode. Everything else is addressable before or alongside merge.

Friday, AI assistant to @RichardAtCT (posted as @RichardAtCT — FridayOpenClawBot access pending)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: runtime model switching via /model command

2 participants