Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 33 additions & 20 deletions unstract/sdk1/src/unstract/sdk1/adapters/base1.py
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,36 @@ def _minimax_context_window(model_id: str) -> int | None:
return None


def _normalize_minimax_thinking(
adapter_metadata: dict[str, "Any"], model_id: str
) -> None:
is_m2_model = _is_minimax_m2_model(model_id)
if "enable_thinking" in adapter_metadata:
enable_thinking = adapter_metadata.pop("enable_thinking")
if not isinstance(enable_thinking, bool):
raise ValueError("enable_thinking must be a boolean.")
if is_m2_model and not enable_thinking:
raise ValueError(f"{model_id} does not support disabling thinking.")
if not is_m2_model:
adapter_metadata["thinking"] = {
"type": "adaptive" if enable_thinking else "disabled"
}

thinking = adapter_metadata.get("thinking")
if thinking is None:
return
if is_m2_model:
raise ValueError(
f"{model_id} uses always-on thinking and does not accept "
"thinking configuration."
)
Comment on lines +586 to +590

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest stripping rather than raising here.

The MiniMax docs say the opposite of this message: "For M2.x models, thinking cannot be disabled; thinking: {"type": "disabled"} is accepted but thinking remains on." The API takes the parameter, it just ignores it.

Practical cost: this turns a currently-working call into a hard failure at LLM.__init__ (llm.py:250), which runs on every completion and on Test Connection — not only at save time. And the form schema has no thinking property, so it can only ever fire for SDK/API callers, who are the least served by a message that contradicts the provider.

The enable_thinking: false raise above is the one worth keeping — that's a user explicitly asking for something impossible.

Suggested change
if is_m2_model:
raise ValueError(
f"{model_id} uses always-on thinking and does not accept "
"thinking configuration."
)
if is_m2_model:
# M2.x always thinks; the provider accepts this parameter but ignores it.
adapter_metadata.pop("thinking")
return

if not isinstance(thinking, dict) or thinking.get("type") not in {
"adaptive",
"disabled",
}:
raise ValueError("thinking.type must be adaptive or disabled.")


class NvidiaBuildLLMParameters(OpenAICompatibleLLMParameters):
"""OpenAI-compatible adapter for NVIDIA's hosted models (build.nvidia.com)."""

Expand Down Expand Up @@ -601,28 +631,11 @@ def validate(adapter_metadata: dict[str, "Any"]) -> dict[str, "Any"]:
if service_tier not in {None, "standard", "priority"}:
raise ValueError("service_tier must be standard or priority.")

if "enable_thinking" in adapter_metadata:
enable_thinking = adapter_metadata.pop("enable_thinking")
if not isinstance(enable_thinking, bool):
raise ValueError("enable_thinking must be a boolean.")
adapter_metadata["thinking"] = {
"type": "adaptive" if enable_thinking else "disabled"
}

thinking = adapter_metadata.get("thinking")
if thinking is None and _is_minimax_m2_model(model_id):
thinking = {"type": "adaptive"}
adapter_metadata["thinking"] = thinking
if thinking is not None:
if not isinstance(thinking, dict) or thinking.get("type") not in {
"adaptive",
"disabled",
}:
raise ValueError("thinking.type must be adaptive or disabled.")
if _is_minimax_m2_model(model_id) and thinking["type"] == "disabled":
raise ValueError(f"{model_id} does not support disabling thinking.")
_normalize_minimax_thinking(adapter_metadata, model_id)

validated = MiniMaxLLMParameters(**adapter_metadata).model_dump()
Comment thread
greptile-apps[bot] marked this conversation as resolved.
if _is_minimax_m2_model(model_id):
validated.pop("thinking", None)
Comment on lines +637 to +638

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only ever deletes a None.

By the time it runs, thinking is guaranteed absent from adapter_metadata for M2 — the helper raises if it was passed, and the enable_thinking branch skips M2 — so Pydantic emits the declared field as None:

MiniMaxLLMParameters(model='minimax/MiniMax-M2.7', api_key='k').model_dump()
  → thinking key present: True | value: None

That's the same None the M3 path keeps and has shipped with since #2166. Popping it only for M2 leaves two shapes for "no thinking" (M2: key absent, M3: key None), which is an easy thing to trip over later.

Suggest deleting both lines. Re-validation stays safe — the helper short-circuits on thinking is None, confirmed with validate({'model': 'MiniMax-M2.7', 'api_key': 'k', 'thinking': None}).

The assertion at line 192 then becomes assert validated["thinking"] is None.

validated["cost_model"] = f"{_MINIMAX_PROVIDER_PREFIX}{model_id}"
if context_window := _minimax_context_window(model_id):
validated["context_window"] = context_window
Expand Down
25 changes: 23 additions & 2 deletions unstract/sdk1/src/unstract/sdk1/adapters/llm1/static/minimax.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,28 @@
"enable_thinking": {
"type": "boolean",
"title": "Enable Thinking",
"description": "Override the protocol default for MiniMax-M3: OpenAI-compatible requests default to adaptive thinking, while Anthropic-compatible requests default to disabled thinking. MiniMax-M2.x models always keep thinking enabled. See [MiniMax API docs](https://platform.minimax.io/docs/api-reference/text-openai-api)."
"description": "Override the protocol default for MiniMax-M3: OpenAI-compatible requests default to adaptive thinking, while Anthropic-compatible requests default to disabled thinking. MiniMax-M2.x models use always-on thinking and only accept this setting as true. See [MiniMax API docs](https://platform.minimax.io/docs/api-reference/text-openai-api)."
}
}
},
"allOf": [
{
"if": {
"properties": {
"model": {
"pattern": "^(?:(?:minimax|anthropic)/)?[Mm][Ii][Nn][Ii][Mm][Aa][Xx]-[Mm]2(?:$|[.-])"
}
},
"required": [
"model"
]
},
"then": {
"properties": {
"enable_thinking": {
"const": true
}
}
}
}
]
Comment on lines +79 to +99

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest dropping this block.

It encodes "what is an M2 model" a third time — _is_minimax_m2_model (base1.py:557) and the prefix-stripping in validate_model are the other two — in a different language, with hand-expanded case-insensitivity because JSON Schema has no /i flag. Nothing fails if the two drift, so the next model family silently loses the hint while tests stay green.

One thing worth knowing before merging either way: I ran this schema through @rjsf/utils@5 + validator-ajv8@5, which is what RjsfFormLayout.jsx uses. The condition does resolve correctly — M2 gets const: true and the checkbox defaults to checked — but unchecking yields two errors, and the second is raw ajv output:

[".enable_thinking must be equal to constant", "must match \"then\" schema"]

transformErrors (RjsfFormLayout.jsx:177) has no if case, so must match "then" schema reaches the user. No other adapter schema in the repo uses allOf/if, so this would become the pattern others copy.

The reworded description just above already conveys always-on, and the backend still rejects enable_thinking: false. If we want form-time feedback later, ui:widget: hidden for M2 reads better than a checkbox that accepts only one value — but it needs the same duplication solved, so it belongs in its own change.

}
44 changes: 42 additions & 2 deletions unstract/sdk1/tests/test_branded_openai_adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,10 +186,27 @@ def test_minimax_m2_rejects_disabling_thinking() -> None:
)


def test_minimax_m2_defaults_to_adaptive_thinking() -> None:
def test_minimax_m2_uses_always_on_thinking_without_request_parameter() -> None:
validated = MiniMaxLLMParameters.validate({"model": "MiniMax-M2.7", "api_key": "k"})

assert validated["thinking"] == {"type": "adaptive"}
assert "thinking" not in validated
assert "thinking" not in MiniMaxLLMParameters.validate(dict(validated))

explicitly_enabled = MiniMaxLLMParameters.validate(
{"model": "MiniMax-M2.7", "api_key": "k", "enable_thinking": True}
)
assert "thinking" not in explicitly_enabled


def test_minimax_m2_rejects_configurable_thinking_payload() -> None:
with pytest.raises(ValueError, match="uses always-on thinking"):
MiniMaxLLMParameters.validate(
{
"model": "MiniMax-M2.7",
"api_key": "k",
"thinking": {"type": "adaptive"},
}
)


def test_minimax_m2_thinking_rules_require_model_family_boundary() -> None:
Expand Down Expand Up @@ -309,6 +326,8 @@ def test_branded_llm_schema_exposes_api_base_with_default(


def test_minimax_schema_covers_models_thinking_and_regions() -> None:
from jsonschema import Draft202012Validator

schema = json.loads(MiniMaxLLMAdapter.get_json_schema())

assert schema["properties"]["model"]["examples"] == [
Expand All @@ -321,6 +340,27 @@ def test_minimax_schema_covers_models_thinking_and_regions() -> None:
"standard",
"priority",
]
assert schema["allOf"][0]["then"]["properties"]["enable_thinking"] == {"const": True}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

allOf[0] couples this to schema layout — add a second conditional and it silently asserts the wrong branch. The Draft202012Validator loop just below already covers the same rule through behaviour, which is the version worth keeping.

Also from jsonschema import Draft202012Validator at line 329 should sit at the top of the file per our import convention.

Both go away if the allOf block is dropped.

validator = Draft202012Validator(schema)
config = {
"adapter_name": "m2",
"api_key": "k",
}
for model in (
"MiniMax-M2.7",
"minimax-m2.7",
"minimax/MiniMax-M2.7",
"anthropic/minimax-m2.7",
):
m2_config = {**config, "model": model}
assert not list(validator.iter_errors({**m2_config, "enable_thinking": True}))
assert list(validator.iter_errors({**m2_config, "enable_thinking": False}))

assert not list(
validator.iter_errors(
{**config, "model": "MiniMax-M20", "enable_thinking": False}
)
)
assert "reasoning_effort" not in json.dumps(schema)


Expand Down