mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-21 17:25:26 -04:00
feat(code): add Grok 4.5 model (#4596)
Add Grok 4.5 to Deep Agents Code model selection with xAI reasoning efforts. Made by [Open SWE](https://openswe.vercel.app/agents/d182a499-a539-da64-2d66-6b487d6519cf) --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
c6c72139b3
commit
b0a209da3a
@@ -27,7 +27,7 @@ type-checked against this alias, so update them in lockstep when it changes.
|
||||
"""
|
||||
|
||||
ReasoningProvider: TypeAlias = Literal[
|
||||
"anthropic", "fireworks", "google_genai", "openai", "openai_codex"
|
||||
"anthropic", "fireworks", "google_genai", "openai", "openai_codex", "xai"
|
||||
]
|
||||
"""Provider identifiers that support model-specific reasoning effort controls.
|
||||
|
||||
@@ -112,9 +112,16 @@ FIREWORKS_GLM_EFFORTS: tuple[EffortLabel, ...] = ("none", "high", "max")
|
||||
See https://docs.fireworks.ai/guides/reasoning.
|
||||
"""
|
||||
|
||||
XAI_EFFORTS: tuple[EffortLabel, ...] = ("low", "medium", "high")
|
||||
"""xAI `reasoning_effort` labels for Grok 4.5.
|
||||
|
||||
See https://docs.x.ai/developers/model-capabilities/text/reasoning.
|
||||
"""
|
||||
|
||||
_REASONING_KEYS: frozenset[str] = frozenset(
|
||||
{
|
||||
"effort",
|
||||
"extra_body",
|
||||
"output_config",
|
||||
"reasoning",
|
||||
"reasoning_effort",
|
||||
@@ -341,6 +348,44 @@ def _fireworks_current_effort(model_params: dict[str, Any]) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def _is_xai_grok_45(model: str) -> bool:
|
||||
"""Return whether `model` is Grok 4.5 or a documented alias."""
|
||||
return model in {"grok-4.5", "grok-4.5-latest", "grok-build-latest"}
|
||||
|
||||
|
||||
def _xai_supported_efforts(model: str) -> tuple[EffortLabel, ...]:
|
||||
"""Return xAI reasoning effort levels for a model."""
|
||||
return XAI_EFFORTS if _is_xai_grok_45(model) else ()
|
||||
|
||||
|
||||
def _xai_default_effort(model: str) -> EffortLabel | None:
|
||||
"""Return the xAI default reasoning effort when known."""
|
||||
return "high" if _is_xai_grok_45(model) else None
|
||||
|
||||
|
||||
def _xai_model_params(effort: str) -> dict[str, Any]:
|
||||
"""Return xAI reasoning params for an effort label."""
|
||||
return {"extra_body": {"reasoning_effort": effort}}
|
||||
|
||||
|
||||
def _xai_current_effort(model_params: dict[str, Any]) -> str | None:
|
||||
"""Read the xAI reasoning effort from model params.
|
||||
|
||||
Returns:
|
||||
The configured effort label, or `None` when unset.
|
||||
"""
|
||||
extra = model_params.get("extra_body")
|
||||
if isinstance(extra, dict):
|
||||
value = extra.get("reasoning_effort")
|
||||
if value is not None and not isinstance(value, str):
|
||||
logger.warning(
|
||||
"Ignoring non-str xAI extra_body.reasoning_effort of type %s",
|
||||
type(value).__name__,
|
||||
)
|
||||
return value if isinstance(value, str) else None
|
||||
return None
|
||||
|
||||
|
||||
_OPENAI_CONFIG = ReasoningProviderConfig(
|
||||
supported_efforts=_openai_supported_efforts,
|
||||
default_effort=_openai_default_effort,
|
||||
@@ -374,6 +419,12 @@ _PROVIDER_CONFIGS: dict[ReasoningProvider, ReasoningProviderConfig] = {
|
||||
model_params=_fireworks_model_params,
|
||||
current_effort=_fireworks_current_effort,
|
||||
),
|
||||
"xai": ReasoningProviderConfig(
|
||||
supported_efforts=_xai_supported_efforts,
|
||||
default_effort=_xai_default_effort,
|
||||
model_params=_xai_model_params,
|
||||
current_effort=_xai_current_effort,
|
||||
),
|
||||
}
|
||||
"""Provider-specific reasoning effort behavior keyed by `ModelSpec` provider."""
|
||||
|
||||
@@ -405,6 +456,8 @@ def _classify_reasoning_provider(provider: str, model: str) -> ReasoningProvider
|
||||
return "google_genai"
|
||||
if provider == "fireworks" and model_lower.startswith("accounts/fireworks/models/"):
|
||||
return "fireworks"
|
||||
if provider == "xai" and _is_xai_grok_45(model_lower):
|
||||
return "xai"
|
||||
return None
|
||||
|
||||
|
||||
@@ -526,7 +579,9 @@ def merge_effort_model_params(
|
||||
"""
|
||||
merged = dict(existing) if existing else {}
|
||||
for key, value in effort_params.items():
|
||||
if key in {"model_kwargs", "output_config"} and isinstance(value, dict):
|
||||
if key in {"extra_body", "model_kwargs", "output_config"} and isinstance(
|
||||
value, dict
|
||||
):
|
||||
current = merged.get(key)
|
||||
base = dict(current) if isinstance(current, dict) else {}
|
||||
base.update(value)
|
||||
@@ -556,7 +611,8 @@ def without_effort_model_params(
|
||||
cleaned = {
|
||||
key: (dict(value) if isinstance(value, dict) else value)
|
||||
for key, value in existing.items()
|
||||
if key not in _REASONING_KEYS and key not in {"model_kwargs", "output_config"}
|
||||
if key not in _REASONING_KEYS
|
||||
and key not in {"extra_body", "model_kwargs", "output_config"}
|
||||
}
|
||||
kwargs = existing.get("model_kwargs")
|
||||
if isinstance(kwargs, dict):
|
||||
@@ -572,4 +628,11 @@ def without_effort_model_params(
|
||||
cleaned["output_config"] = output_config_params
|
||||
elif output_config is not None:
|
||||
cleaned["output_config"] = output_config
|
||||
extra = existing.get("extra_body")
|
||||
if isinstance(extra, dict):
|
||||
extra_params = {k: v for k, v in extra.items() if k != "reasoning_effort"}
|
||||
if extra_params:
|
||||
cleaned["extra_body"] = extra_params
|
||||
elif extra is not None:
|
||||
cleaned["extra_body"] = extra
|
||||
return cleaned or None
|
||||
|
||||
@@ -114,6 +114,7 @@ _RECOMMENDED_MODELS: dict[str, str] = {
|
||||
"openrouter:openrouter/fusion": "OpenRouter Fusion",
|
||||
"openrouter:qwen/qwen3.7-plus": "Qwen 3.7 Plus",
|
||||
"openrouter:z-ai/glm-5.2": "GLM 5.2",
|
||||
"xai:grok-4.5": "Grok 4.5",
|
||||
}
|
||||
"""Hand-curated frontier-tier models promoted across the UI, mapped to a
|
||||
human-readable display name.
|
||||
|
||||
@@ -65,6 +65,9 @@ def _restore_settings() -> Iterator[None]:
|
||||
("anthropic:claude-opus-4-16", ("low", "medium", "high", "xhigh", "max")),
|
||||
("google_genai:gemini-3.5-flash", ("low", "medium", "high")),
|
||||
("google_genai:gemini-3.1-pro-preview", ("low", "medium", "high")),
|
||||
("xai:grok-4.5", ("low", "medium", "high")),
|
||||
("xai:grok-4.5-latest", ("low", "medium", "high")),
|
||||
("xai:grok-build-latest", ("low", "medium", "high")),
|
||||
(
|
||||
"fireworks:accounts/fireworks/models/deepseek-v4-pro",
|
||||
("none", "low", "medium", "high", "xhigh", "max"),
|
||||
@@ -81,6 +84,7 @@ def _restore_settings() -> Iterator[None]:
|
||||
("openai_codex:gpt-4o", ()),
|
||||
("anthropic:claude-3-5-haiku-latest", ()),
|
||||
("google_genai:gemini-2.5-flash", ()),
|
||||
("xai:grok-4", ()),
|
||||
("fireworks:accounts/fireworks/models/llama-v3p1-70b-instruct", ()),
|
||||
],
|
||||
)
|
||||
@@ -104,6 +108,7 @@ def test_supported_efforts_for_model(model_spec: str, efforts: tuple[str, ...])
|
||||
("google_genai:gemini-3.1-pro-preview", "high"),
|
||||
("google_genai:gemini-3-pro", "high"),
|
||||
("google_genai:gemini-3-flash", "high"),
|
||||
("xai:grok-4.5", "high"),
|
||||
("fireworks:accounts/fireworks/models/deepseek-v4-pro", "high"),
|
||||
("fireworks:accounts/fireworks/models/glm-5p2", "max"),
|
||||
("fireworks:accounts/fireworks/models/kimi-k2p7-code", None),
|
||||
@@ -128,6 +133,9 @@ def test_model_params_for_effort_maps_provider_kwargs() -> None:
|
||||
assert model_params_for_effort(
|
||||
"fireworks:accounts/fireworks/models/deepseek-v4-pro", "max"
|
||||
) == {"model_kwargs": {"reasoning_effort": "max"}}
|
||||
assert model_params_for_effort("xai:grok-4.5", "medium") == {
|
||||
"extra_body": {"reasoning_effort": "medium"}
|
||||
}
|
||||
|
||||
|
||||
def test_current_effort_reads_anthropic_output_config() -> None:
|
||||
@@ -139,6 +147,15 @@ def test_current_effort_reads_anthropic_output_config() -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_current_effort_reads_xai_extra_body() -> None:
|
||||
assert (
|
||||
current_effort_from_model_params(
|
||||
"xai:grok-4.5", {"extra_body": {"reasoning_effort": "high"}}
|
||||
)
|
||||
== "high"
|
||||
)
|
||||
|
||||
|
||||
def test_model_params_for_effort_rejects_unsupported_effort() -> None:
|
||||
assert (
|
||||
model_params_for_effort(
|
||||
@@ -171,6 +188,21 @@ def test_merge_and_clear_effort_model_params_preserves_unrelated_params() -> Non
|
||||
}
|
||||
|
||||
|
||||
def test_merge_and_clear_xai_effort_preserves_extra_body_params() -> None:
|
||||
merged = merge_effort_model_params(
|
||||
{"extra_body": {"prompt_cache_key": "thread-1"}},
|
||||
{"extra_body": {"reasoning_effort": "high"}},
|
||||
)
|
||||
|
||||
assert merged == {
|
||||
"extra_body": {"prompt_cache_key": "thread-1", "reasoning_effort": "high"}
|
||||
}
|
||||
assert current_effort_from_model_params("xai:grok-4.5", merged) == "high"
|
||||
assert without_effort_model_params(merged) == {
|
||||
"extra_body": {"prompt_cache_key": "thread-1"}
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("model_spec", "model_params"),
|
||||
[
|
||||
@@ -185,6 +217,7 @@ def test_merge_and_clear_effort_model_params_preserves_unrelated_params() -> Non
|
||||
"fireworks:accounts/fireworks/models/deepseek-v4-pro",
|
||||
{"model_kwargs": {"reasoning_effort": 5}},
|
||||
),
|
||||
("xai:grok-4.5", {"extra_body": {"reasoning_effort": 5}}),
|
||||
],
|
||||
)
|
||||
def test_current_effort_warns_on_malformed_params(
|
||||
@@ -215,6 +248,13 @@ def test_current_effort_non_dict_model_kwargs_is_silent() -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_current_effort_non_dict_extra_body_is_silent() -> None:
|
||||
"""A non-dict `extra_body` is a legit shape and must not warn."""
|
||||
assert (
|
||||
current_effort_from_model_params("xai:grok-4.5", {"extra_body": "raw"}) is None
|
||||
)
|
||||
|
||||
|
||||
def test_effort_argument_hint_covers_effort_vocabulary() -> None:
|
||||
"""The `/effort` argument hint must list every `EffortLabel` plus a reset.
|
||||
|
||||
|
||||
@@ -2251,15 +2251,21 @@ class TestGetModelDisplayName:
|
||||
== "claude-sonnet-4-5"
|
||||
)
|
||||
|
||||
def test_uses_recommended_name_when_no_profile(self) -> None:
|
||||
@pytest.mark.parametrize(
|
||||
("spec", "name"),
|
||||
[
|
||||
("fireworks:accounts/fireworks/models/kimi-k2p7-code", "Kimi K2.7 Code"),
|
||||
("xai:grok-4.5", "Grok 4.5"),
|
||||
],
|
||||
)
|
||||
def test_uses_recommended_name_when_no_profile(self, spec: str, name: str) -> None:
|
||||
"""Uninstalled recommendations use the hardcoded name, not the raw id."""
|
||||
from deepagents_code.tui.widgets import model_selector
|
||||
|
||||
screen = ModelSelectorScreen.__new__(ModelSelectorScreen)
|
||||
screen._profiles = {}
|
||||
spec = "fireworks:accounts/fireworks/models/kimi-k2p7-code"
|
||||
assert spec in model_selector._RECOMMENDED_MODELS
|
||||
assert screen._get_model_display_name(spec) == "Kimi K2.7 Code"
|
||||
assert screen._get_model_display_name(spec) == name
|
||||
|
||||
def test_profile_name_wins_over_recommended_name(self) -> None:
|
||||
"""A loaded profile's `name` takes precedence over the hardcoded one."""
|
||||
|
||||
Reference in New Issue
Block a user