diff --git a/plugins/model-providers/opencode-zen/__init__.py b/plugins/model-providers/opencode-zen/__init__.py index ebf3b9274d6f..22f00ddd3f06 100644 --- a/plugins/model-providers/opencode-zen/__init__.py +++ b/plugins/model-providers/opencode-zen/__init__.py @@ -31,6 +31,18 @@ def _is_deepseek_thinking_model(model: str | None) -> bool: return m == "deepseek-reasoner" +def _is_glm_thinking_model(model: str | None) -> bool: + """Return True for GLM models that expose reasoning controls on OpenCode Go. + + GLM-5 model IDs currently route through this profile, but Hermes was + treating them like generic chat-completions models and silently dropping + `/reasoning`. Reuse the existing DeepSeek-style mapping so GLM-5 requests + can forward ``reasoning_effort`` or the fallback ``extra_body.thinking`` + fields instead of falling through this gate. + """ + return _flat_model_name(model).startswith("glm-5") + + class OpenCodeGoProfile(ProviderProfile): """OpenCode Go - model-specific reasoning controls.""" @@ -80,7 +92,10 @@ def build_api_kwargs_extras( extra_body["thinking"] = {"type": "enabled"} return extra_body, top_level - if not _is_deepseek_thinking_model(model): + if not ( + _is_deepseek_thinking_model(model) + or _is_glm_thinking_model(model) + ): return extra_body, top_level enabled = True diff --git a/tests/plugins/model_providers/test_opencode_go_profile.py b/tests/plugins/model_providers/test_opencode_go_profile.py index fa28a77db06e..6064fc403b96 100644 --- a/tests/plugins/model_providers/test_opencode_go_profile.py +++ b/tests/plugins/model_providers/test_opencode_go_profile.py @@ -122,13 +122,59 @@ def test_xhigh_and_max_normalize_to_max(self, opencode_go_profile): assert top_level == {"reasoning_effort": "max"} +class TestOpenCodeGoGLMThinking: + """GLM-5 models use the same thinking contract on OpenCode Go.""" + + def test_high_effort_emits_thinking_and_effort(self, opencode_go_profile): + extra_body, top_level = opencode_go_profile.build_api_kwargs_extras( + reasoning_config={"enabled": True, "effort": "high"}, + model="glm-5.1", + ) + assert extra_body == {} + assert top_level == {"reasoning_effort": "high"} + + def test_disabled_emits_thinking_disabled_without_effort(self, opencode_go_profile): + extra_body, top_level = opencode_go_profile.build_api_kwargs_extras( + reasoning_config={"enabled": False}, + model="glm-5.1", + ) + assert extra_body == {"thinking": {"type": "disabled"}} + assert top_level == {} + + def test_no_config_emits_thinking_enabled_without_effort(self, opencode_go_profile): + extra_body, top_level = opencode_go_profile.build_api_kwargs_extras( + reasoning_config=None, + model="glm-5.1", + ) + assert extra_body == {"thinking": {"type": "enabled"}} + assert top_level == {} + + def test_minimal_effort_enables_thinking_without_effort(self, opencode_go_profile): + extra_body, top_level = opencode_go_profile.build_api_kwargs_extras( + reasoning_config={"enabled": True, "effort": "minimal"}, + model="zai/glm-5.2", + ) + assert extra_body == {"thinking": {"type": "enabled"}} + assert top_level == {} + + def test_xhigh_and_max_normalize_to_max(self, opencode_go_profile): + for effort in ("xhigh", "max"): + extra_body, top_level = opencode_go_profile.build_api_kwargs_extras( + reasoning_config={"enabled": True, "effort": effort}, + model="z-ai/glm-5.2", + ) + assert extra_body == {} + assert top_level == {"reasoning_effort": "max"} + + class TestOpenCodeGoModelGating: - """Other OpenCode Go models must not receive Kimi/DeepSeek controls.""" + """Other OpenCode Go models must not receive Kimi/DeepSeek/GLM controls.""" @pytest.mark.parametrize( "model", [ - "glm-5.1", + "glm-4.5", + "glm-4.5-flash", "qwen3.6-plus", "minimax-m2.7", "deepseek-v3.1", @@ -178,3 +224,17 @@ def test_deepseek_thinking_reaches_extra_body_and_top_level( ) assert "extra_body" not in kwargs assert kwargs["reasoning_effort"] == "high" + + def test_glm_thinking_reaches_extra_body_and_top_level(self, opencode_go_profile): + from agent.transports.chat_completions import ChatCompletionsTransport + + kwargs = ChatCompletionsTransport().build_kwargs( + model="glm-5.1", + messages=[{"role": "user", "content": "ping"}], + tools=None, + provider_profile=opencode_go_profile, + reasoning_config={"enabled": True, "effort": "high"}, + base_url="https://opencode.ai/zen/go/v1", + ) + assert "extra_body" not in kwargs + assert kwargs["reasoning_effort"] == "high"