Skip to content

feat(cli): allow switching toolcall model at runtime (#1182)#1192

Merged
muddlebee merged 5 commits intoTracer-Cloud:mainfrom
Davidson3556:fix/cli-switch-toolcall-model-1182
May 4, 2026
Merged

feat(cli): allow switching toolcall model at runtime (#1182)#1192
muddlebee merged 5 commits intoTracer-Cloud:mainfrom
Davidson3556:fix/cli-switch-toolcall-model-1182

Conversation

@Davidson3556
Copy link
Copy Markdown
Contributor

Fixes #1182

Describe the changes you have made in this PR -

The interactive shell now exposes a runtime-supported way to change the toolcall model, matching the "Expected behavior" in the issue.

  • New flag: /model set <provider> [model] --toolcall-model <model> — switch provider, reasoning model, and toolcall model in one command.
  • New subcommand: /model toolcall set <model> — change only the toolcall model on the currently active provider, leaving the reasoning model untouched.
  • The connection table's toolcall model row now reflects the value the user just set (it was previously stuck at the default).
  • Providers that don't expose a separate toolcall model (codex, claude-code, ollama) reject the override with a clear yellow message instead of silently lying.
  • The natural-language assistant action schema now accepts an optional toolcall_model on switch_llm_provider, plus a new switch_toolcall_model action — so requests like "switch the toolcall model also to claude-opus-4-7" actually work instead of returning the "OpenSRE doesn't currently expose a separate action..." refusal shown in the bug report.

Files touched:

  • app/cli/wizard/config.py — added toolcall_model_env and default_toolcall_model to ProviderOption; populated for anthropic/openai/openrouter/gemini/nvidia.
  • app/cli/interactive_shell/commands.py — extended switch_llm_provider, added switch_toolcall_model, added a small flag parser for /model set, added /model toolcall subcommand, and updated the /model help text.
  • app/cli/interactive_shell/cli_agent.py — updated _ACTION_RULE and _execute_action_plan so the LLM-driven assistant can request a toolcall switch.
  • tests/cli/interactive_shell/test_commands.py — added 5 tests covering the flag, the subcommand, the unsupported-provider rejection, and the missing-arg usage paths.

Demo/Screenshot for feature changes and bug fixes -

Screenshot 2026-05-01 at 20 13 30

Code Understanding and AI Usage

Did you use AI assistance (ChatGPT, Claude, Copilot, etc.) to write any part of this code?

  • No, I wrote all the code myself
  • Yes, I used AI assistance (continue below)

If you used AI assistance:

  • I have reviewed every single line of the AI-generated code
  • I can explain the purpose and logic of each function/component I added
  • I have tested edge cases and understand how the code handles them
  • I have modified the AI output to follow this project's coding standards and conventions

Explain your implementation approach:

The bug was a write/read asymmetry: LLMSettings.from_env() already reads <PROVIDER>_TOOLCALL_MODEL, and the connection table already renders that value, but switch_llm_provider only ever wrote LLM_PROVIDER and the reasoning model env var — so the toolcall row was effectively read-only from the user's perspective.

Two alternatives I considered and rejected:

  1. Auto-mirror the toolcall model from the reasoning model on every /model set. Rejected because it silently destroys the user's explicit toolcall override and makes the two values uncontrollable independently — the connection table would still display two rows but they'd always be equal.
  2. Add a separate /toolcall top-level slash command. Rejected because the discoverability story is worse: users already type /model when thinking about models, and the help text for one command is easier to maintain than two.

Chosen approach: keep /model as the single entry point. Add a flag (--toolcall-model) for the combined case and a subcommand (/model toolcall set) for the toolcall-only case, both of which funnel through the same env-sync path that already handles the reasoning model. Provider metadata gains a toolcall_model_env field so each provider declares which env var holds its toolcall model; CLI providers (codex/claude-code/ollama) set it to None and the helper returns a polite rejection instead of writing nothing or pretending it worked.

Key functions:

  • switch_llm_provider(provider, console, model=None, *, toolcall_model=None) — extended with a keyword-only toolcall_model. When provided, it adds provider.toolcall_model_env=<value> to the dict that sync_env_values writes, so the reasoning + toolcall + provider all land in .env atomically.
  • switch_toolcall_model(model, console, *, provider_name=None) — new helper. Resolves the active provider from LLM_PROVIDER (defaulting to anthropic, mirroring how the rest of the shell defaults), looks up toolcall_model_env, and writes only that one key. This is what makes /model toolcall set non-destructive to the reasoning model.
  • _parse_model_set_args(args) — small flag parser. Returns None for malformed input (unknown flag, missing flag value, two positional models) so /model set can print a single usage line instead of guessing.
  • _cmd_model — adds a toolcall branch and routes set through the new parser.
  • _ACTION_RULE / _execute_action_plan in cli_agent.py — taught the assistant about the new action shapes, including a label that shows both reasoning and toolcall in the action preview ("switch LLM provider to anthropic (claude-opus-4-7) + toolcall claude-opus-4-7").

Edge cases tested:

  • Unknown flag (--made-up-flag) → prints usage, no env write.
  • Toolcall-only switch on a provider with no separate toolcall env (codex) → prints "does not expose a separate toolcall model", no env write.
  • /model toolcall set with no model → prints usage.
  • Toolcall-only switch must NOT rewrite LLM_PROVIDER or the reasoning model — verified by asserting absence of those keys in the resulting .env.
  • Combined /model set anthropic claude-opus-4-7 --toolcall-model claude-opus-4-7 writes all three keys.

Checklist before requesting a review

  • I have added proper PR title and linked to the issue
  • I have performed a self-review of my code
  • I can explain the purpose of every function, class, and logic block I added
  • I understand why my changes work and have tested them thoroughly
  • I have considered potential edge cases and how my code handles them
  • If it is a core feature, I have added thorough tests
  • My code follows the project's style guidelines and conventions

Note: Please check Allow edits from maintainers if you would like us to assist in the PR.

`/model set <provider>` only persisted the reasoning model, leaving the
toolcall model stuck at its default and giving users no documented way
to change it from the interactive shell.

Add a `--toolcall-model` flag to `/model set` and a `/model toolcall set
<model>` subcommand that writes `<PROVIDER>_TOOLCALL_MODEL` to .env via
the existing env_sync path. Provider metadata gains a
`toolcall_model_env` field; CLI providers (codex/claude-code/ollama) set
it to None and reject toolcall overrides. The NL action schema also
accepts an optional `toolcall_model` and a new `switch_toolcall_model`
shape so the assistant can honor "switch the toolcall model also to X".
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 2, 2026

Greptile Summary

This PR wires up runtime toolcall-model switching in the interactive shell, fixing the write/read asymmetry where LLMSettings.from_env() already read <PROVIDER>_TOOLCALL_MODEL but /model set never wrote it. It adds --toolcall-model to /model set, a new /model toolcall set subcommand, corresponding natural-language assistant actions, and a credential-presence guard that prevents half-updated .env files on provider switch.

Confidence Score: 5/5

Safe to merge; all findings are P2 style/UX nits with no impact on correctness in current callers.

No P0 or P1 issues found. The three P2 comments cover: extra-arg handling inconsistency between /model toolcall set and /model set, a confusing error when a flag-looking string is passed as the provider, and a latent silent no-op for empty-string toolcall that cannot currently be triggered by any existing caller. Core logic, credential guard, env write, and session recording are all correct.

app/cli/interactive_shell/commands.py — minor edge-case handling in _parse_model_set_args and the toolcall subcommand branch of _cmd_model.

Important Files Changed

Filename Overview
app/cli/interactive_shell/commands.py Adds switch_toolcall_model, extends switch_llm_provider with keyword-only toolcall_model, and adds _parse_model_set_args; minor inconsistency in extra-arg handling for /model toolcall set vs /model set, and a latent silent no-op for empty-string toolcall input.
app/cli/interactive_shell/cli_agent.py Updates _ACTION_RULE schema and _execute_action_plan to handle switch_toolcall_model and toolcall_model on switch_llm_provider; control-flow and session-recording logic looks correct.
app/cli/wizard/config.py Adds toolcall_model_env field to ProviderOption dataclass; correctly populated for the five API-backed providers and left as None (default) for CLI-backed providers.
tests/cli/interactive_shell/test_commands.py Adds well-structured tests for the new flag, subcommand, unsupported-provider rejection, missing-arg paths, and credential guard; covers the key happy and sad paths.
tests/cli/interactive_shell/test_cli_agent.py Patches ANTHROPIC_API_KEY in two existing tests to satisfy the new credential guard added to switch_llm_provider; no new test logic, correctly adapts existing tests.

Reviews (2): Last reviewed commit: "fix(cli): label reasoning vs toolcall sl..." | Re-trigger Greptile

Comment thread app/cli/wizard/config.py Outdated
Comment thread app/cli/interactive_shell/commands.py Outdated
…oud#1182)

Greptile flagged two P2s on PR Tracer-Cloud#1192:

- `default_toolcall_model` was added to `ProviderOption` and populated
  for all five API-backed providers, but neither `switch_llm_provider`
  nor `switch_toolcall_model` ever read it. The toolcall env var either
  comes from a prior user override or `LLMSettings.from_env()`'s static
  default constants, so the field was dead code. Remove it (and the now
  unused TOOLCALL_MODEL imports).

- `--toolcall` was silently accepted as a shorthand alias for
  `--toolcall-model` in `_parse_model_set_args` but missing from the
  usage line, the SLASH_COMMANDS help text, and the action label, which
  would surprise users who copy it from source. Drop the alias.
@Davidson3556
Copy link
Copy Markdown
Contributor Author

@davincios @rrajan94 @VaibhavUpreti kindly review

Comment thread app/cli/interactive_shell/commands.py Outdated
Reviewer (Tracer-Cloud#1192) asked for a more specific error than the generic
usage line when `/model set anthropic --toolcall-model` is typed
without a value. Today the parser returns None and the dispatcher
prints the same usage line for every malformed input, which leaves
the user guessing what went wrong.

Switch `_parse_model_set_args` to raise `ValueError` with a targeted
message ("missing value for --toolcall-model", "unknown flag: <flag>",
"unexpected extra argument: <token>", "missing provider name"). The
dispatcher prints the message in red on the first line and the usage
template on a dim follow-up line, so the user sees both *what* broke
and *what shape* the command should take.
@Davidson3556
Copy link
Copy Markdown
Contributor Author

@Devesh36 done

Copy link
Copy Markdown
Collaborator

@yashksaini-coder yashksaini-coder left a comment

Choose a reason for hiding this comment

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

image

Did a local test, even though we don't have an anthropic key, the function still executes, shows that it switched LLM provider, and updates the provider, which should not be the case.

The expected behaviour should be that it throws error saying env vars not setup or provided, and must stop from updating the LLM provider config in settings.

Copy link
Copy Markdown
Collaborator

@Devesh36 Devesh36 left a comment

Choose a reason for hiding this comment

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

Image

how is reasoning model changing then ?

…racer-Cloud#1182)

Reviewer (Tracer-Cloud#1192) caught that /model set anthropic happily updates .env
and prints "switched LLM provider" even when ANTHROPIC_API_KEY is not
set, leaving the user in a half-broken state where the very next
/model show prints "LLM settings unavailable".

Add a precheck in switch_llm_provider that calls has_llm_api_key on
the target provider's api_key_env BEFORE writing to .env or os.environ,
and return False with a specific error + a copy-pastable export hint
when the credential is missing. Skip the check for providers whose
credential is not a secret (ollama / OLLAMA_HOST has a working default)
and for CLI-backed providers (codex, claude-code) that authenticate
through the vendor CLI and have no api_key_env.

Existing /model set tests now have to opt in by setting a fake
ANTHROPIC_API_KEY since the helper used to be credential-blind.
…utput (Tracer-Cloud#1182)

Reviewer (Tracer-Cloud#1192) ran `/model set anthropic claude-haiku-4-5-20251001`
and asked "how is the reasoning model changing then?" because the
success line read

  switched LLM provider: anthropic (claude-haiku-4-5-20251001)

with no indication of which slot the parenthetical model went into.
The /model show table below it then showed reasoning and toolcall both
equal to that model (toolcall was already haiku, reasoning had just
changed), which made it look like one command had silently overwritten
both. It hadn't — the parser treats the second positional as the
reasoning model and the toolcall row was unchanged — but the message
made the right behavior look wrong.

Always label both slots on a switch:

  switched LLM provider: anthropic
  reasoning model: claude-haiku-4-5-20251001 (ANTHROPIC_REASONING_MODEL)
  [toolcall model: ... (ANTHROPIC_TOOLCALL_MODEL)]   # only when --toolcall-model was passed
@Davidson3556
Copy link
Copy Markdown
Contributor Author

@Devesh36 the behavior here is actually correct, but I see why it read like a bug. The second positional argument in
/model set <provider> [model] [--toolcall-model <m>] is the reasoning model (per /help and the usage line). In your repro:

before: reasoning = claude-sonnet-4-6
toolcall = claude-haiku-4-5-20251001

you ran: /model set anthropic claude-haiku-4-5-20251001
^^^^^^^^^^^^^^^^^^^^^^^^^
this is the reasoning model

after: reasoning = claude-haiku-4-5-20251001 (just changed)
toolcall = claude-haiku-4-5-20251001 (unchanged ,was
already haiku)

So both rows show the same value because you happened to pick a reasoning model that matches the existing toolcall default. The tool call slot itself was not rewritten.

That said, you were right to flag it , the success message switched LLM provider: anthropic (claude-haiku-4-5-20251001) didn't tell you which slot the parenthetical landed in, which is what made it look wrong. Pushed 8ec5892 to fix the wording. The new output
is:

switched LLM provider: anthropic
reasoning model: claude-haiku-4-5-20251001 (ANTHROPIC_REASONING_MODEL)
[toolcall model: (ANTHROPIC_TOOLCALL_MODEL)] # only when
# --toolcall-model
# was passed
updated /home/.../.env

So next time the source of each row in the connection table is unambiguous from the lines above it.

For reference, the three forms now all behave distinctly:

/model set anthropic # changes reasoning only
/model toolcall set # changes toolcall only
/model set anthropic --toolcall-model # changes both

@Davidson3556
Copy link
Copy Markdown
Contributor Author

@yashksaini-coder fixed.

Copy link
Copy Markdown
Collaborator

@yashksaini-coder yashksaini-coder left a comment

Choose a reason for hiding this comment

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

All requested changes addressed (commits e66ad5b, 8ec5892). Credential gate, explicit slot labelling, and specific parse errors are tested. LGTM.

@VaibhavUpreti
Copy link
Copy Markdown
Member

@greptileai review

@muddlebee muddlebee merged commit e470e68 into Tracer-Cloud:main May 4, 2026
10 checks passed
@muddlebee
Copy link
Copy Markdown
Collaborator

@Davidson3556 this is great add on 🔥

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 4, 2026

🌮 @Davidson3556's PR: showed up unannounced, improved everything, left zero bugs. Just like a perfect taco. 🌮


👋 Join us on Discord - OpenSRE : hang out, contribute, or hunt for features and issues. Everyone's welcome.

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.

Allow CLI to switch toolcall model at runtime

5 participants