Skip to content

/model: probe effort combo on switch, stop passing reasoning_effort to litellm - #62

Merged
akseljoonas merged 2 commits into
mainfrom
model-switcher-probe
Apr 22, 2026
Merged

/model: probe effort combo on switch, stop passing reasoning_effort to litellm#62
akseljoonas merged 2 commits into
mainfrom
model-switcher-probe

Conversation

@akseljoonas

Copy link
Copy Markdown
Contributor

Summary

  • Switching to claude-opus-4-7 with /effort set was 400ing because litellm 1.83.0 doesn't know 4.7's thinking API shape (thinking.type.adaptive + output_config.effort). Rather than maintain a per-model capability table that rots on every Claude release, this PR trusts the API itself: send Anthropic-native thinking params directly, and validate model+effort with a 1-token ping on /model switch. If the provider rejects a level, a cascade walks max → xhigh → high → medium → low until something sticks, and the result is cached per-model for the session.
  • /effort gains xhigh and max; default preference bumped from high to max (user asked for "best effort per model, usually the highest"). Non-thinking models get thinking stripped automatically. New models from any family should Just Work — no table updates required.
  • Safety net in agent_loop heals & retries once if a real call hits a thinking/effort 400 mid-conversation (e.g. after /effort change).

Key design choices

  • No model catalog. The Anthropic/OpenAI/HF APIs are the source of truth. Probe classifies 400s via error-string matchers; unknown responses bubble up.
  • Bypass litellm's reasoning_effort → thinking translator for Anthropic. We pass thinking + output_config as top-level kwargs; litellm forwards unknown top-level params into the Anthropic body (confirmed by live probe). extra_body does not work here — Anthropic rejects it as "Extra inputs are not permitted".
  • One localized monkey-patch in llm_params.py widens litellm 1.83's hardcoded _is_opus_4_6_model substring check so effort=max isn't pre-flight-rejected for 4.7. Self-documenting, self-removes once the pin catches up to litellm PR #25867 (merged on main, not yet in a PyPI stable).

Live test

Ran the probe against a mix of obscure HF models + Anthropic direct:

Model Result
anthropic/claude-opus-4-7 ✓ max (1 attempt, 1.8s)
anthropic/claude-haiku-4-5 ✓ off — thinking stripped (3 attempts, 241ms)
deepseek-ai/DeepSeek-R1 ✓ high (max not supported, using high)
Qwen/Qwen3.5-9B, meta-llama/Llama-3.1-8B-Instruct ✓ high
XiaomiMiMo/MiMo-V2-Flash, google/gemma-4-31B-it ✓ high
katanemo/Arch-Router-1.5B (1.5B!), moonshotai/Kimi-K2.5 ✓ high
anthropic/does-not-exist ✗ NotFoundError — switch correctly rejected, current model preserved

Test plan

  • uv run python -m agent.main, then /model anthropic/claude-opus-4-7 — expect effort:max, ~1–2s
  • /model anthropic/claude-haiku-4-5 — expect effort:off with "doesn't support reasoning" note
  • /model deepseek-ai/DeepSeek-R1 — expect effort:high with "max not supported" note
  • /model anthropic/does-not-exist — expect red "Switch failed" and current model preserved
  • /effort xhigh then /effort — preference shows, per-model cache clears
  • Send a real message after any successful switch — no thinking.type.enabled 400

Known limitation

xhigh on Opus 4.7 is unreachable via the probe until litellm's internal valid-effort list is updated (it still hardcodes {high, medium, low, max} and rejects xhigh synchronously). Cascade walks to high. Unlocks automatically once the litellm pin moves forward; our monkey-patch becomes a no-op at that point and can be deleted.

…o litellm

Switching to claude-opus-4-7 with /effort set would 400 with
"thinking.type.enabled is not supported" because litellm 1.83.0's
Anthropic adapter substring-matches "4.6" to decide which thinking API
shape to send, and doesn't know about 4.7's adaptive + output_config.effort
contract. Rather than maintain a per-model capability table that rots
every time a new Claude family ships, this change trusts the API itself:

- llm_params.py: for anthropic/*, bypass litellm's reasoning_effort -> thinking
  mapping and pass thinking={type: adaptive} plus output_config={effort: ...}
  as top-level kwargs directly. litellm forwards unknown top-level params
  into Anthropic request bodies (extra_body does NOT work here — Anthropic
  rejects it as "Extra inputs are not permitted"). One localized monkey-patch
  widens litellm 1.83's hardcoded _is_opus_4_6_model check so effort=max
  isn't rejected synchronously on 4.7 — removable once litellm ships PR
  #25867 upstream.

- effort_probe.py: new probe that fires a 1-token ping on /model switch
  with the same params we'd use for real, walking a cascade
  max -> xhigh -> high -> medium -> low until the provider stops rejecting.
  Three outcomes: success (cache the level), thinking-unsupported (cache
  None, strip on future calls), inconclusive (switch anyway with warning).
  Persistent non-thinking 4xx (auth, model-not-found) bubbles up so
  /model rejects the switch and keeps the current model.

- session.py: per-model effective_effort cache + effective_effort_for()
  helper. Populated by the probe, read by the real LLM call so resolved
  levels don't re-probe on every message. /effort change invalidates.

- agent_loop.py: safety net — if a real call 400s with thinking/effort
  config errors mid-conversation (e.g. after /effort change without
  re-probe), heal the cache and retry once before propagating.

- main.py: default reasoning_effort = "max" (was "high"); /model runs
  the probe and prints (effort: X, Nms); /effort accepts xhigh and max
  and shows per-model probed ceilings; SUGGESTED_MODELS includes Opus 4.7.

Live-tested against Opus 4.7, Haiku 4.5, DeepSeek-R1, Qwen3.5-9B,
Llama-3.1-8B, MiMo-V2-Flash, Gemma-4-31B, Arch-Router-1.5B, Kimi-K2.5,
and a non-existent id. All outcomes matched expectations.
main.py was accumulating model-switch specifics (suggested list, id
format check, HF routing info printer, probe-and-switch flow, commit
helper). Moving them to a dedicated module keeps the REPL dispatcher
focused on input parsing and makes the switcher independently testable.

Net: main.py down ~160 lines, /model handler is now a 10-line delegation.
No behavior change.
@akseljoonas
akseljoonas merged commit e2552e8 into main Apr 22, 2026
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.

1 participant