diff --git a/solstone/think/providers/local.py b/solstone/think/providers/local.py index 1385fe146..258513731 100644 --- a/solstone/think/providers/local.py +++ b/solstone/think/providers/local.py @@ -246,7 +246,7 @@ def _build_request_body( max_output_tokens: int, json_output: bool, json_schema: dict | None, - apply_qwen_sampling: bool, + is_bundled: bool, ) -> dict[str, Any]: body: dict[str, Any] = { "model": model_id, @@ -254,11 +254,11 @@ def _build_request_body( "temperature": temperature, "max_tokens": max_output_tokens, "stream": False, - "chat_template_kwargs": {"enable_thinking": False}, } - if apply_qwen_sampling: + if is_bundled: body.update( { + "chat_template_kwargs": {"enable_thinking": False}, "top_p": _QWEN_TOP_P, "top_k": _QWEN_TOP_K, "min_p": _QWEN_MIN_P, diff --git a/solstone/think/providers/openhands.py b/solstone/think/providers/openhands.py index 7334eb7a7..22e9cc0d3 100644 --- a/solstone/think/providers/openhands.py +++ b/solstone/think/providers/openhands.py @@ -86,6 +86,7 @@ _LOCAL_OUTPUT_RESERVE_TOKENS = LOCAL_MIN_CONTEXT_TOKENS // 4 _LOCAL_CONDENSER_MAX_TOKENS = LOCAL_MIN_CONTEXT_TOKENS * 11 // 16 _LOCAL_CONDENSER_KEEP_FIRST = 4 _GENERATE_NUM_RETRIES = 2 +_PROBE_MAX_OUTPUT_TOKENS = 16 _GEMINI_MAX_OUTPUT_TOKENS = 65_535 _ANTHROPIC_THINKING_BUFFER = 1_000 _SCHEMA_NAME_RE = re.compile(r"^[a-zA-Z0-9_-]{1,64}$") @@ -195,7 +196,6 @@ def _build_llm(provider: str, model: str) -> Any: retry_multiplier=1.0, input_cost_per_token=0, output_cost_per_token=0, - litellm_extra_body={"chat_template_kwargs": {"enable_thinking": False}}, ) from solstone.think.providers import local_server @@ -1970,7 +1970,7 @@ def _probe(provider: str, model: str, api_key: str) -> None: model, provider=provider, temperature=None, - max_output_tokens=8, + max_output_tokens=_PROBE_MAX_OUTPUT_TOKENS, system_instruction=None, json_output=False, thinking_budget=None, diff --git a/tests/openhands_fakes.py b/tests/openhands_fakes.py index 8c14dc238..f1de5e237 100644 --- a/tests/openhands_fakes.py +++ b/tests/openhands_fakes.py @@ -175,6 +175,18 @@ class LLM(FakeModel): return self.responses(messages, **kwargs) +class ImageContent(FakeModel): + pass + + +class Message(FakeModel): + pass + + +class TextContent(FakeModel): + pass + + class Agent(FakeModel): pass @@ -243,6 +255,7 @@ def install_fake_openhands(monkeypatch: Any) -> types.SimpleNamespace: sdk_mod = types.ModuleType("openhands.sdk") event_mod = types.ModuleType("openhands.sdk.event") conversation_error_mod = types.ModuleType("openhands.sdk.event.conversation_error") + llm_mod = types.ModuleType("openhands.sdk.llm") tool_mod = types.ModuleType("openhands.sdk.tool") schema_mod = types.ModuleType("openhands.sdk.tool.schema") builtins_mod = types.ModuleType("openhands.sdk.tool.builtins") @@ -253,6 +266,9 @@ def install_fake_openhands(monkeypatch: Any) -> types.SimpleNamespace: sdk_mod.LLM = LLM sdk_mod.Agent = Agent sdk_mod.Conversation = Conversation + llm_mod.ImageContent = ImageContent + llm_mod.Message = Message + llm_mod.TextContent = TextContent event_mod.ActionEvent = ActionEvent event_mod.ObservationEvent = ObservationEvent event_mod.MessageEvent = MessageEvent @@ -269,6 +285,7 @@ def install_fake_openhands(monkeypatch: Any) -> types.SimpleNamespace: root_mod.sdk = sdk_mod sdk_mod.event = event_mod + sdk_mod.llm = llm_mod sdk_mod.tool = tool_mod event_mod.conversation_error = conversation_error_mod tool_mod.schema = schema_mod @@ -282,6 +299,7 @@ def install_fake_openhands(monkeypatch: Any) -> types.SimpleNamespace: "openhands.sdk": sdk_mod, "openhands.sdk.event": event_mod, "openhands.sdk.event.conversation_error": conversation_error_mod, + "openhands.sdk.llm": llm_mod, "openhands.sdk.tool": tool_mod, "openhands.sdk.tool.schema": schema_mod, "openhands.sdk.tool.builtins": builtins_mod, @@ -302,4 +320,7 @@ def install_fake_openhands(monkeypatch: Any) -> types.SimpleNamespace: ConversationErrorEvent=ConversationErrorEvent, Conversation=Conversation, LLM=LLM, + ImageContent=ImageContent, + Message=Message, + TextContent=TextContent, ) diff --git a/tests/test_local.py b/tests/test_local.py index 4d94b1128..196c4c24c 100644 --- a/tests/test_local.py +++ b/tests/test_local.py @@ -959,6 +959,14 @@ def test_run_generate_byo_posts_to_normalized_endpoint_and_skips_connect(monkeyp assert captured["url"] == "http://byo.example/openai/v1/chat/completions" assert captured["json"]["model"] == "served-model" + for key in ( + "chat_template_kwargs", + "top_p", + "top_k", + "min_p", + "presence_penalty", + ): + assert key not in captured["json"] assert captured["headers"] == {"Authorization": "Bearer test-token-PLACEHOLDER"} assert local_budget.TRUNCATION_MARKER not in str(captured["json"]) assert result["text"] == "hello" @@ -1422,7 +1430,7 @@ def test_run_agenerate_confidential_with_stray_slots_skips_admission(monkeypatch assert captured["timeout"] == pytest.approx(1.0) -def test_run_generate_byo_body_omits_bundled_qwen_sampling(monkeypatch): +def test_run_generate_byo_body_omits_bundled_request_fields(monkeypatch): provider = _provider() monkeypatch.setattr(provider, "resolve_local_endpoint", _byo_endpoint) captured_posts = [] @@ -1476,7 +1484,6 @@ def test_run_generate_byo_body_omits_bundled_qwen_sampling(monkeypatch): "temperature": 0.4, "max_tokens": 7, "stream": False, - "chat_template_kwargs": {"enable_thinking": False}, } assert captured_posts[1]["json"] == { "model": "served-model", @@ -1484,7 +1491,6 @@ def test_run_generate_byo_body_omits_bundled_qwen_sampling(monkeypatch): "temperature": 0.5, "max_tokens": 11, "stream": False, - "chat_template_kwargs": {"enable_thinking": False}, "response_format": { "type": "json_schema", "json_schema": { @@ -1495,7 +1501,13 @@ def test_run_generate_byo_body_omits_bundled_qwen_sampling(monkeypatch): }, } for post in captured_posts: - for key in ("top_p", "top_k", "min_p", "presence_penalty"): + for key in ( + "chat_template_kwargs", + "top_p", + "top_k", + "min_p", + "presence_penalty", + ): assert key not in post["json"] @@ -2204,9 +2216,9 @@ def test_openhands_local_byo_llm_kwargs(monkeypatch, credential, expected_key): "retry_multiplier": 1.0, "input_cost_per_token": 0, "output_cost_per_token": 0, - "litellm_extra_body": {"chat_template_kwargs": {"enable_thinking": False}}, } assert "max_input_tokens" not in captured + assert "litellm_extra_body" not in captured waits = [ min( captured["retry_max_wait"], diff --git a/tests/test_openhands_generate.py b/tests/test_openhands_generate.py index 52152205c..b3dce1888 100644 --- a/tests/test_openhands_generate.py +++ b/tests/test_openhands_generate.py @@ -382,6 +382,28 @@ async def test_run_agenerate_transport_kwargs_do_not_shadow_llm_timeout( assert "timeout" not in getattr(llm, transport_attr) +def test_validation_probe_uses_minimum_output_budget(fake_openhands): + assert openhands._PROBE_MAX_OUTPUT_TOKENS == 16 + + assert openhands.validate_key("openai", "key") == {"valid": True} + assert ( + fake_openhands.LLM.instances[-1].max_output_tokens + == openhands._PROBE_MAX_OUTPUT_TOKENS + ) + + assert openhands.validate_key("google", "key") == {"valid": True} + assert ( + fake_openhands.LLM.instances[-1].max_output_tokens + == openhands._PROBE_MAX_OUTPUT_TOKENS + ) + + assert openhands.validate_key("anthropic", "key") == {"valid": True} + assert ( + fake_openhands.LLM.instances[-1].max_output_tokens + == openhands._PROBE_MAX_OUTPUT_TOKENS + ) + + def test_validation_uses_runtime_probe_and_classifies_results(monkeypatch): monkeypatch.setattr(openhands, "_probe", lambda *args: None) assert openhands.validate_key("google", "key") == {"valid": True}