lmstudio_reasoning.py
1 """LM Studio reasoning-effort resolution shared by the chat-completions 2 transport and run_agent's iteration-limit summary path. 3 4 LM Studio publishes per-model ``capabilities.reasoning.allowed_options`` (e.g. 5 ``["off","on"]`` for toggle-style models, ``["off","minimal","low"]`` for 6 graduated models). We map the user's ``reasoning_config`` onto LM Studio's 7 OpenAI-compatible vocabulary, then clamp against the model's allowed set so 8 the server doesn't 400 on an unsupported effort. 9 """ 10 11 from __future__ import annotations 12 13 from typing import List, Optional 14 15 # LM Studio accepts these top-level reasoning_effort values via its 16 # OpenAI-compatible chat.completions endpoint. 17 _LM_VALID_EFFORTS = {"none", "minimal", "low", "medium", "high", "xhigh"} 18 19 # Toggle-style models publish allowed_options as ["off","on"] in /api/v1/models. 20 # Map them onto the OpenAI-compatible request vocabulary. 21 _LM_EFFORT_ALIASES = {"off": "none", "on": "medium"} 22 23 24 def resolve_lmstudio_effort( 25 reasoning_config: Optional[dict], 26 allowed_options: Optional[List[str]], 27 ) -> Optional[str]: 28 """Return the ``reasoning_effort`` string to send to LM Studio, or ``None``. 29 30 ``None`` means "omit the field": the user picked a level the model can't 31 honor, so let LM Studio fall back to the model's declared default rather 32 than silently substituting a different effort. When ``allowed_options`` is 33 falsy (probe failed), skip clamping and send the resolved effort anyway. 34 """ 35 effort = "medium" 36 if reasoning_config and isinstance(reasoning_config, dict): 37 if reasoning_config.get("enabled") is False: 38 effort = "none" 39 else: 40 raw = (reasoning_config.get("effort") or "").strip().lower() 41 raw = _LM_EFFORT_ALIASES.get(raw, raw) 42 if raw in _LM_VALID_EFFORTS: 43 effort = raw 44 if allowed_options: 45 allowed = {_LM_EFFORT_ALIASES.get(opt, opt) for opt in allowed_options} 46 if effort not in allowed: 47 return None 48 return effort