diff --git a/docs/advanced/configuration.mdx b/docs/advanced/configuration.mdx
index af98b8b8..027f6d47 100644
--- a/docs/advanced/configuration.mdx
+++ b/docs/advanced/configuration.mdx
@@ -36,7 +36,7 @@ Configure Strix using environment variables or a config file.
- Control thinking effort for reasoning models. Valid values: `none`, `minimal`, `low`, `medium`, `high`, `xhigh`. Defaults to `medium` for quick scan mode.
+ Control thinking effort for reasoning models. Valid values: `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`. Defaults to `medium` for quick scan mode.
diff --git a/strix/config/models.py b/strix/config/models.py
index 1c84ef3c..a3bb481a 100644
--- a/strix/config/models.py
+++ b/strix/config/models.py
@@ -83,10 +83,13 @@ class _CodexResponsesModel(OpenAIResponsesModel):
effort = self._reasoning_effort
if effort and effort != "none":
# Clamp to efforts the backend accepts.
- if effort == "minimal":
- effort = "low"
- elif effort == "xhigh":
- effort = "high"
+ match effort:
+ case "minimal":
+ effort = "low"
+ case "xhigh" | "max":
+ effort = "high"
+ case _:
+ pass
overrides = overrides.resolve(ModelSettings(reasoning=Reasoning(effort=effort)))
return model_settings.resolve(overrides)
diff --git a/strix/config/settings.py b/strix/config/settings.py
index e53d125c..0f26cd62 100644
--- a/strix/config/settings.py
+++ b/strix/config/settings.py
@@ -8,7 +8,7 @@ from pydantic import AliasChoices, Field
from pydantic_settings import BaseSettings, SettingsConfigDict
-ReasoningEffort = Literal["none", "minimal", "low", "medium", "high", "xhigh"]
+ReasoningEffort = Literal["none", "minimal", "low", "medium", "high", "xhigh", "max"]
_BASE_CONFIG = SettingsConfigDict(
case_sensitive=False,
diff --git a/strix/core/inputs.py b/strix/core/inputs.py
index 34a2d4b3..81f46076 100644
--- a/strix/core/inputs.py
+++ b/strix/core/inputs.py
@@ -147,7 +147,7 @@ def make_model_settings(
and model_supports_reasoning(model_name)
):
model_settings = model_settings.resolve(
- ModelSettings(reasoning=Reasoning(effort=reasoning_effort)),
+ _reasoning_settings(reasoning_effort, model_settings.extra_args),
)
if force_required_tool_choice and _accepts_required_tool_choice(model_name):
model_settings = model_settings.resolve(ModelSettings(tool_choice="required"))
@@ -162,6 +162,22 @@ def make_model_settings(
return model_settings
+def _reasoning_settings(
+ effort: ReasoningEffort,
+ extra_args: dict[str, Any] | None,
+) -> ModelSettings:
+ """``max`` is not in the OpenAI SDK's ``Reasoning.effort`` enum, so send it as
+ a raw body field instead — also keeping it clear of LiteLLM's DeepSeek mapping,
+ which collapses every ``reasoning_effort`` level to plain thinking-enabled.
+ Providers that don't support ``max`` reject the request.
+ """
+ if effort != "max":
+ return ModelSettings(reasoning=Reasoning(effort=effort))
+ return ModelSettings(
+ extra_args={**(extra_args or {}), "extra_body": {"reasoning_effort": "max"}},
+ )
+
+
def _prompt_cache_extra_args(model_name: str) -> dict[str, Any] | None:
"""LiteLLM ``cache_control_injection_points`` for Claude prompt caching.
diff --git a/strix/interface/main.py b/strix/interface/main.py
index 4d88beda..abe77fa3 100644
--- a/strix/interface/main.py
+++ b/strix/interface/main.py
@@ -171,8 +171,8 @@ def validate_environment() -> None:
error_text.append("• ", style="white")
error_text.append("STRIX_REASONING_EFFORT", style="bold cyan")
error_text.append(
- " - Reasoning effort level: none, minimal, low, medium, high, xhigh "
- "(default: high)\n",
+ " - Reasoning effort level: none, minimal, low, medium, high, xhigh, "
+ "max (default: high)\n",
style="white",
)
diff --git a/tests/test_inputs.py b/tests/test_inputs.py
index 871ea149..83a431ad 100644
--- a/tests/test_inputs.py
+++ b/tests/test_inputs.py
@@ -129,6 +129,17 @@ def test_prompt_cache_kept_for_non_bedrock_claude_even_if_unmapped(monkeypatch:
]
+def test_max_reasoning_effort_sent_as_raw_body_field() -> None:
+ # "max" is absent from the OpenAI SDK's Reasoning enum, and LiteLLM's DeepSeek
+ # mapping collapses every effort to thinking-enabled, so it has to ride along
+ # as a raw body field to reach the provider.
+ settings = make_model_settings(
+ "max", model_name="deepseek/deepseek-v4-flash", request_timeout=30
+ )
+ assert settings.reasoning is None
+ assert settings.extra_args == {"timeout": 30, "extra_body": {"reasoning_effort": "max"}}
+
+
def test_conversation_tail_breakpoint_moves_with_appended_transcript() -> None:
# LiteLLM must place the index=-1 cache_control on the last message however
# long the transcript grows.