feat(providers): first-class Mistral support
Mistral's API constrains reasoning_effort to "high"/"none", rejects the
kwarg entirely for Magistral (reasoning is implicit), returns assistant
content as a mixed array of {type:"thinking",...}/{type:"text",...}
blocks, and 400s on the reasoning_content key in history.
- Remap user-supplied reasoning_effort (low/medium/minimal) onto Mistral's
two-tier vocabulary; strip the kwarg for Magistral models
- Lift thinking blocks into reasoning_content for both batch and streaming
responses; pass only text through on_content_delta callbacks
- Drop reasoning_content from outbound history when the spec asks for it
- Expose per-preset reasoning_effort_values so the UI can render the
provider-specific option set
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
committed by
Xubin Ren
co-authored by
Claude Sonnet 4.6
parent
51bd3337ef
commit
d5f5eb43e5
@@ -654,6 +654,40 @@ def _image_generation_provider_rows(config: Any) -> list[dict[str, Any]]:
|
||||
return rows
|
||||
|
||||
|
||||
_DEFAULT_REASONING_EFFORT_VALUES: tuple[str, ...] = ("", "low", "medium", "high")
|
||||
|
||||
|
||||
def _reasoning_effort_values_for(provider_name: str, model: str) -> list[str]:
|
||||
"""Return user-facing reasoning_effort options for this provider+model.
|
||||
|
||||
Mistral chat models accept only "high"/"none"; Magistral rejects the
|
||||
kwarg entirely (reasoning is implicit). For everyone else, return the
|
||||
full OpenAI vocab.
|
||||
"""
|
||||
spec = find_by_name(provider_name) if provider_name else None
|
||||
if spec is None:
|
||||
return list(_DEFAULT_REASONING_EFFORT_VALUES)
|
||||
|
||||
model_lower = (model or "").lower()
|
||||
implicit = getattr(spec, "implicit_reasoning_models", ())
|
||||
if implicit and any(pat in model_lower for pat in implicit):
|
||||
# Reasoning is always on; only "Default" makes sense.
|
||||
return [""]
|
||||
|
||||
remap = getattr(spec, "reasoning_effort_remap", ())
|
||||
if remap:
|
||||
# Reverse the remap: surface the distinct wire-vocab outputs as the
|
||||
# user's options. Mistral collapses to "high"/"none" → UI shows
|
||||
# "Default" + "High".
|
||||
wire_values: list[str] = []
|
||||
for _user_val, wire_val in remap:
|
||||
if wire_val and wire_val != "none" and wire_val not in wire_values:
|
||||
wire_values.append(wire_val)
|
||||
return ["", *wire_values]
|
||||
|
||||
return list(_DEFAULT_REASONING_EFFORT_VALUES)
|
||||
|
||||
|
||||
def _transcription_provider_rows(config: Any) -> list[dict[str, Any]]:
|
||||
rows: list[dict[str, Any]] = []
|
||||
for name in transcription_provider_names():
|
||||
@@ -741,6 +775,9 @@ def settings_payload(
|
||||
"context_window_tokens": defaults.context_window_tokens,
|
||||
"temperature": defaults.temperature,
|
||||
"reasoning_effort": defaults.reasoning_effort,
|
||||
"reasoning_effort_values": _reasoning_effort_values_for(
|
||||
defaults.provider, defaults.model
|
||||
),
|
||||
}
|
||||
]
|
||||
for name, preset in config.model_presets.items():
|
||||
@@ -756,6 +793,9 @@ def settings_payload(
|
||||
"context_window_tokens": preset.context_window_tokens,
|
||||
"temperature": preset.temperature,
|
||||
"reasoning_effort": preset.reasoning_effort,
|
||||
"reasoning_effort_values": _reasoning_effort_values_for(
|
||||
preset.provider, preset.model
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user