[?2004l[1;38mdiff --git a/openhands-sdk/openhands/sdk/llm/llm.py b/openhands-sdk/openhands/sdk/llm/llm.py[m
[1;38mindex 1b95893f95..7a29fae91a 100644[m
[1;38m--- a/openhands-sdk/openhands/sdk/llm/llm.py[m
[1;38m+++ b/openhands-sdk/openhands/sdk/llm/llm.py[m
@@ -109,7 +109,7 @@
 from openhands.sdk.llm.utils.image_resize import maybe_resize_messages_for_provider
 from openhands.sdk.llm.utils.litellm_provider import infer_litellm_provider
 from openhands.sdk.llm.utils.metrics import Metrics
[31m-from openhands.sdk.llm.utils.model_features import get_features[m
[32m+from openhands.sdk.llm.utils.model_features import ModelFeatures, get_features[m
 from openhands.sdk.llm.utils.openhands_provider import (
     LiteLLMCallKwargs,
     canonicalize_openhands_llm_payload,
@@ -390,6 +390,27 @@ class LLM(BaseModel, RetryMixin, NonNativeToolCallingMixin):
         ),
         json_schema_extra=field_meta(),
     )
[32m+    api_mode: Literal["auto", "chat", "responses"] = Field([m
[32m+        default="auto",[m
[32m+        description=([m
[32m+            "LLM API endpoint mode. 'auto' resolves from model metadata and "[m
[32m+            "SDK fallbacks; use 'chat' or 'responses' to override endpoint "[m
[32m+            "selection for proxy aliases and newly released models."[m
[32m+        ),[m
[32m+        json_schema_extra=field_meta(),[m
[32m+    )[m
[32m+    capability_overrides: dict[str, bool | str] = Field([m
[32m+        default_factory=dict,[m
[32m+        description=([m
[32m+            "Explicit model capability overrides. Supported keys include "[m
[32m+            "supports_reasoning_effort, thinking_mode (adaptive, manual, none, "[m
[32m+            "or unknown), supports_sampling_params, supports_prompt_cache, "[m
[32m+            "supports_stop_words, supports_responses_api, supports_vision, and "[m
[32m+            "supports_prompt_cache_retention. Overrides take precedence over "[m
[32m+            "LiteLLM metadata and SDK fallbacks."[m
[32m+        ),[m
[32m+        json_schema_extra=field_meta(),[m
[32m+    )[m
     extra_headers: dict[str, str] | None = Field(
         default=None,
         description="Optional HTTP headers to forward to LiteLLM requests.",
@@ -491,12 +512,15 @@ class LLM(BaseModel, RetryMixin, NonNativeToolCallingMixin):
         ),
         json_schema_extra=field_meta(),
     )
[31m-    reasoning_effort: Literal["low", "medium", "high", "xhigh", "none"] | None = Field([m
[32m+    reasoning_effort: ([m
[32m+        Literal["low", "medium", "high", "xhigh", "none"] | SkipJsonSchema[str] | None[m
[32m+    ) = Field([m
         default="high",
[31m-        description="The effort to put into reasoning. "[m
[31m-        "This is a string that can be one of 'low', 'medium', 'high', 'xhigh', "[m
[31m-        "or 'none'. "[m
[31m-        "Can apply to all reasoning models.",[m
[32m+        description=([m
[32m+            "Provider-neutral reasoning effort. Common values include 'none', "[m
[32m+            "'minimal', 'low', 'medium', 'high', 'xhigh', and 'max'. The SDK "[m
[32m+            "accepts future provider values and lets LiteLLM translate them."[m
[32m+        ),[m
         json_schema_extra=field_meta(),
     )
     reasoning_summary: Literal["auto", "concise", "detailed"] | None = Field(
@@ -524,8 +548,11 @@ class LLM(BaseModel, RetryMixin, NonNativeToolCallingMixin):
     )
     extended_thinking_budget: int | None = Field(
         default=200_000,
[31m-        description="The budget tokens for extended thinking, "[m
[31m-        "supported by Anthropic models.",[m
[32m+        description=([m
[32m+            "Legacy token budget for models confirmed to use manual Anthropic "[m
[32m+            "extended thinking. Ignored for adaptive-thinking models. Prefer "[m
[32m+            "reasoning_effort for new integrations."[m
[32m+        ),[m
         json_schema_extra=field_meta(),
     )
     seed: int | None = Field(
@@ -2287,6 +2314,14 @@ def _model_name_for_capabilities(self) -> str:
         """Return canonical name for capability lookups (e.g., vision support)."""
         return self.model_canonical_name or self.model
 
[32m+    def _model_features(self) -> ModelFeatures:[m
[32m+        """Resolve capabilities consistently for every request path."""[m
[32m+        return get_features([m
[32m+            self._model_name_for_capabilities(),[m
[32m+            model_info=self._model_info,[m
[32m+            overrides=self.capability_overrides,[m
[32m+        )[m
[32m+[m
     def _provider_has_joint_token_budget(self) -> bool:
         """Whether the provider enforces input + max_tokens <= context_window.
 
@@ -2519,10 +2554,7 @@ def _validate_context_window_size(self) -> None:
             )
 
     def vision_is_active(self) -> bool:
[31m-        return not self.disable_vision and ([m
[31m-            get_features(self._model_name_for_capabilities()).supports_vision[m
[31m-            or bool(self._model_info and self._model_info.get("supports_vision", False))[m
[31m-        )[m
[32m+        return not self.disable_vision and self._model_features().supports_vision[m
 
     def is_caching_prompt_active(self) -> bool:
         """Check if prompt caching is supported and enabled for current model.
@@ -2533,18 +2565,14 @@ def is_caching_prompt_active(self) -> bool:
         """
         if not self.caching_prompt:
             return False
[31m-        # We don't need to look up model_info because explicit caching[m
[31m-        # breakpoint support is tracked in the local feature table.[m
[31m-        return ([m
[31m-            self.caching_prompt[m
[31m-            and get_features(self._model_name_for_capabilities()).supports_prompt_cache[m
[31m-        )[m
[32m+        return self.caching_prompt and self._model_features().supports_prompt_cache[m
 
     def uses_responses_api(self) -> bool:
         """Whether this model uses the OpenAI Responses API path."""
 
[31m-        # by default, uses = supports[m
[31m-        return get_features(self._model_name_for_capabilities()).supports_responses_api[m
[32m+        if self.api_mode != "auto":[m
[32m+            return self.api_mode == "responses"[m
[32m+        return self._model_features().supports_responses_api[m
 
     @property
     def model_info(self) -> dict | None:
@@ -2606,9 +2634,7 @@ def _inline_required(self) -> bool:
         """Resolve whether http(s) image URLs must be downloaded and inlined."""
         if self.inline_image_urls is not None:
             return self.inline_image_urls
[31m-        return get_features([m
[31m-            self._model_name_for_capabilities()[m
[31m-        ).requires_inline_image_data[m
[32m+        return self._model_features().requires_inline_image_data[m
 
     def _begin_chat_messages(
         self, messages: list[Message]
@@ -2644,7 +2670,7 @@ def _prepare_chat_messages(self, messages: list[Message]) -> list[Message]:
         return messages
 
     def _to_chat_dicts(self, messages: list[Message]) -> list[dict]:
[31m-        model_features = get_features(self._model_name_for_capabilities())[m
[32m+        model_features = self._model_features()[m
         cache_enabled = self.is_caching_prompt_active()
         vision_enabled = self.vision_is_active()
         function_calling_enabled = self.native_tool_calling
[1;38mdiff --git a/openhands-sdk/openhands/sdk/llm/mixins/non_native_fc.py b/openhands-sdk/openhands/sdk/llm/mixins/non_native_fc.py[m
[1;38mindex adbbe54c3f..8ecc3c7a93 100644[m
[1;38m--- a/openhands-sdk/openhands/sdk/llm/mixins/non_native_fc.py[m
[1;38m+++ b/openhands-sdk/openhands/sdk/llm/mixins/non_native_fc.py[m
@@ -12,7 +12,7 @@
     convert_fncall_messages_to_non_fncall_messages,
     convert_non_fncall_messages_to_fncall_messages,
 )
[31m-from openhands.sdk.llm.utils.model_features import get_features[m
[32m+from openhands.sdk.llm.utils.model_features import ModelFeatures[m
 
 
 class _HostSupports(Protocol):
@@ -20,6 +20,8 @@ class _HostSupports(Protocol):
     disable_stop_word: bool | None
     native_tool_calling: bool
 
[32m+    def _model_features(self) -> ModelFeatures: ...[m
[32m+[m
 
 class NonNativeToolCallingMixin:
     """Mixin providing prompt-mocked tool-calling support when native FC is off.
@@ -54,7 +56,7 @@ def pre_request_prompt_mock(
             add_in_context_learning_example=add_iclex,
             include_security_params=include_security_params,
         )
[31m-        if get_features(self.model).supports_stop_words and not self.disable_stop_word:[m
[32m+        if self._model_features().supports_stop_words and not self.disable_stop_word:[m
             kwargs = dict(kwargs)
             kwargs["stop"] = STOP_WORDS
 
[1;38mdiff --git a/openhands-sdk/openhands/sdk/llm/options/chat_options.py b/openhands-sdk/openhands/sdk/llm/options/chat_options.py[m
[1;38mindex 721053bc94..63adabb9bd 100644[m
[1;38m--- a/openhands-sdk/openhands/sdk/llm/options/chat_options.py[m
[1;38m+++ b/openhands-sdk/openhands/sdk/llm/options/chat_options.py[m
@@ -8,7 +8,6 @@
     apply_extra_body,
     apply_extra_headers,
 )
[31m-from openhands.sdk.llm.utils.model_features import get_features[m
 
 
 if TYPE_CHECKING:
@@ -43,24 +42,24 @@ def select_chat_options(
 
     out = apply_extra_headers(out, llm)
 
[31m-    # Reasoning-model quirks[m
[31m-    supports_reasoning_effort = get_features(llm.model).supports_reasoning_effort[m
[32m+    model_features = llm._model_features()[m
[32m+    supports_reasoning_effort = model_features.supports_reasoning_effort[m
     if supports_reasoning_effort:
[31m-        # LiteLLM automatically handles reasoning_effort for all models, including[m
[31m-        # Claude Opus 4.5 (maps to output_config and adds beta header automatically)[m
         if llm.reasoning_effort is not None:
             out["reasoning_effort"] = llm.reasoning_effort
 
[31m-        # All reasoning models ignore temp/top_p, except Gemini[m
[31m-        if "gemini" not in llm.model.lower():[m
[31m-            out.pop("temperature", None)[m
[31m-            out.pop("top_p", None)[m
[32m+    model_name = llm._model_name_for_capabilities()[m
[32m+    if model_features.supports_sampling_params is False or ([m
[32m+        model_features.supports_sampling_params is None[m
[32m+        and supports_reasoning_effort[m
[32m+        and "gemini" not in model_name.lower()[m
[32m+    ):[m
[32m+        out.pop("temperature", None)[m
[32m+        out.pop("top_p", None)[m
[32m+        out.pop("top_k", None)[m
 
[31m-    # Extended thinking models[m
[31m-    if get_features(llm.model).supports_extended_thinking:[m
[32m+    if model_features.thinking_mode == "manual":[m
         if llm.extended_thinking_budget and max_output_tokens:
[31m-            # Anthropic throws errors if thinking budget equals or exceeds max output[m
[31m-            # tokens -- force the thinking budget lower if there's a conflict[m
             budget_tokens = min(
                 llm.extended_thinking_budget,
                 max_output_tokens - 1,
@@ -69,18 +68,15 @@ def select_chat_options(
                 "type": "enabled",
                 "budget_tokens": budget_tokens,
             }
[31m-            # Enable interleaved thinking[m
[31m-            # Merge default header with any user-provided headers; user wins on conflict[m
             existing = out.get("extra_headers") or {}
             out["extra_headers"] = {
                 "anthropic-beta": "interleaved-thinking-2025-05-14",
                 **existing,
             }
[31m-            # Fix litellm behavior[m
             out["max_tokens"] = max_output_tokens
[31m-        # Anthropic models ignore temp/top_p[m
         out.pop("temperature", None)
         out.pop("top_p", None)
[32m+        out.pop("top_k", None)[m
 
     # Tools: if not using native, strip tool_choice so we don't confuse providers
     if not has_tools:
@@ -88,10 +84,7 @@ def select_chat_options(
         out.pop("tool_choice", None)
 
     # Send prompt_cache_retention only if model supports it
[31m-    if ([m
[31m-        get_features(llm.model).supports_prompt_cache_retention[m
[31m-        and llm.prompt_cache_retention[m
[31m-    ):[m
[32m+    if model_features.supports_prompt_cache_retention and llm.prompt_cache_retention:[m
         out["prompt_cache_retention"] = llm.prompt_cache_retention
 
     out = apply_extra_body(out, llm)
[1;38mdiff --git a/openhands-sdk/openhands/sdk/llm/options/responses_options.py b/openhands-sdk/openhands/sdk/llm/options/responses_options.py[m
[1;38mindex 4ad698014a..a9b8c00275 100644[m
[1;38m--- a/openhands-sdk/openhands/sdk/llm/options/responses_options.py[m
[1;38m+++ b/openhands-sdk/openhands/sdk/llm/options/responses_options.py[m
@@ -8,7 +8,6 @@
     apply_extra_body,
     apply_extra_headers,
 )
[31m-from openhands.sdk.llm.utils.model_features import get_features[m
 
 
 if TYPE_CHECKING:
@@ -31,10 +30,13 @@ def select_responses_options(
         defaults["max_output_tokens"] = llm.effective_max_output_tokens
     out = apply_defaults_if_absent(user_kwargs, defaults)
 
[31m-    # Enforce sampling/tool behavior for Responses path[m
[31m-    # Note: temperature is not supported in subscription mode[m
[31m-    if not llm.is_subscription:[m
[31m-        out["temperature"] = 1.0[m
[32m+    model_features = llm._model_features()[m
[32m+    if not llm.is_subscription and model_features.supports_sampling_params is False:[m
[32m+        out.pop("temperature", None)[m
[32m+        out.pop("top_p", None)[m
[32m+        out.pop("top_k", None)[m
[32m+    elif not llm.is_subscription and llm.temperature is not None:[m
[32m+        out.setdefault("temperature", llm.temperature)[m
     out["tool_choice"] = "auto"
 
     out = apply_extra_headers(out, llm)
@@ -45,8 +47,6 @@ def select_responses_options(
     else:
         out.setdefault("store", False)
 
[31m-    model_features = get_features(llm._model_name_for_capabilities())[m
[31m-[m
     # Include encrypted reasoning only when the user enables it on the LLM,
     # and only for stateless calls (store=False). Respect user choice.
     # Note: include and reasoning are not supported in subscription mode
[1;38mdiff --git a/openhands-sdk/openhands/sdk/llm/utils/model_features.py b/openhands-sdk/openhands/sdk/llm/utils/model_features.py[m
[1;38mindex 825ccc4d50..6edc9bf752 100644[m
[1;38m--- a/openhands-sdk/openhands/sdk/llm/utils/model_features.py[m
[1;38m+++ b/openhands-sdk/openhands/sdk/llm/utils/model_features.py[m
@@ -1,7 +1,8 @@
 import warnings
[31m-from collections.abc import Iterable[m
[32m+from collections.abc import Iterable, Mapping[m
 from dataclasses import dataclass
 from functools import cache
[32m+from typing import<response clipped><NOTE>Due to the max output limit, only part of the full response has been shown to you.</NOTE>     "supports_reasoning": True,[m
[32m+            "supports_adaptive_thinking": True,[m
[32m+            "supports_sampling_params": False,[m
[32m+        },[m
[32m+    )[m
[32m+[m
[32m+    out = select_chat_options(llm, user_kwargs={}, has_tools=True)[m
[32m+[m
[32m+    assert out["reasoning_effort"] == "high"[m
[32m+    assert "temperature" not in out[m
[32m+    assert "thinking" not in out[m
[32m+[m
[32m+[m
[32m+def test_chat_options_sampling_override_takes_precedence():[m
[32m+    llm = DummyLLM([m
[32m+        model="litellm_proxy/future-reasoning-model",[m
[32m+        model_canonical_name="anthropic/claude-sonnet-5",[m
[32m+        top_k=40,[m
[32m+        top_p=0.9,[m
[32m+        temperature=0.7,[m
[32m+        reasoning_effort="high",[m
[32m+        model_info={[m
[32m+            "supports_reasoning": True,[m
[32m+            "supports_adaptive_thinking": True,[m
[32m+        },[m
[32m+        capability_overrides={"supports_sampling_params": True},[m
[32m+    )[m
[32m+[m
[32m+    out = select_chat_options(llm, user_kwargs={}, has_tools=True)[m
[32m+[m
[32m+    assert out["temperature"] == 0.7[m
[32m+    assert out["top_p"] == 0.9[m
[32m+    assert out["top_k"] == 40[m
[32m+    assert out["reasoning_effort"] == "high"[m
[32m+[m
[32m+[m
[32m+@pytest.mark.parametrize([m
[32m+    "model,provider",[m
[32m+    [[m
[32m+        ("claude-sonnet-5", "anthropic"),[m
[32m+        ("us.anthropic.claude-sonnet-5-v1:0", "bedrock"),[m
[32m+        ("claude-opus-5", "anthropic"),[m
[32m+        ("us.anthropic.claude-opus-5-v1:0", "bedrock"),[m
[32m+    ],[m
[32m+)[m
[32m+def test_litellm_translates_claude_5_reasoning_to_adaptive_thinking([m
[32m+    model: str, provider: str[m
[32m+):[m
[32m+    params = get_optional_params([m
[32m+        model=model,[m
[32m+        custom_llm_provider=provider,[m
[32m+        reasoning_effort="high",[m
[32m+    )[m
[32m+[m
[32m+    assert params["thinking"] == {"type": "adaptive"}[m
[32m+    assert "budget_tokens" not in params["thinking"][m
 
 
 def test_bedrock_opus_4_8_strips_temp_top_p_without_thinking_block():
[31m-    """Bedrock cross-region claude-opus-4-8 routes through the reasoning path.[m
[31m-[m
[31m-    LiteLLM does not (yet) recognize the Bedrock cross-region inference id as a[m
[31m-    reasoning model, so the SDK-side override must mark it as one. It must take[m
[31m-    the reasoning_effort path (which strips temperature/top_p) and NOT the[m
[31m-    extended-thinking path, which would inject the legacy[m
[31m-    ``thinking.type=enabled`` block + ``interleaved-thinking`` header that[m
[31m-    Anthropic now rejects for this model (see reverted #3427 / revert #3441).[m
[31m-    """[m
     llm = DummyLLM(
         model="bedrock/us.anthropic.claude-opus-4-8-v1:0",
         top_p=1.0,  # SDK default
[1;38mdiff --git a/tests/sdk/llm/test_model_canonical_name_resolution.py b/tests/sdk/llm/test_model_canonical_name_resolution.py[m
[1;38mindex 2c914d5392..d27932ac27 100644[m
[1;38m--- a/tests/sdk/llm/test_model_canonical_name_resolution.py[m
[1;38m+++ b/tests/sdk/llm/test_model_canonical_name_resolution.py[m
@@ -29,7 +29,7 @@ def fake_get_model_info(secret_api_key, base_url, model):
             return {"supports_vision": True, "max_input_tokens": 128000}
         return None
 
[31m-    def fake_get_features(model: str):[m
[32m+    def fake_get_features(model: str, model_info=None, overrides=None):[m
         feature_calls.append(model)
         return DummyFeatures(model)
 
@@ -71,3 +71,27 @@ def test_model_canonical_name_with_real_model_info():
     assert proxied.vision_is_active() == base.vision_is_active()
     assert proxied.is_caching_prompt_active() == base.is_caching_prompt_active()
     assert proxied.uses_responses_api() == base.uses_responses_api()
[32m+[m
[32m+[m
[32m+def test_api_mode_can_override_endpoint_discovery():[m
[32m+    assert LLM(model="gpt-4o", api_mode="responses").uses_responses_api() is True[m
[32m+    assert LLM(model="gpt-5", api_mode="chat").uses_responses_api() is False[m
[32m+[m
[32m+[m
[32m+def test_reasoning_effort_accepts_forward_compatible_values():[m
[32m+    assert LLM(model="future-model", reasoning_effort="max").reasoning_effort == "max"[m
[32m+    assert ([m
[32m+        LLM(model="future-model", reasoning_effort="future-tier").reasoning_effort[m
[32m+        == "future-tier"[m
[32m+    )[m
[32m+[m
[32m+[m
[32m+def test_reasoning_effort_preserves_legacy_json_schema_enum():[m
[32m+    schema = LLM.model_json_schema()["properties"]["reasoning_effort"][m
[32m+    enum_values = next([m
[32m+        branch["enum"][m
[32m+        for branch in schema["anyOf"][m
[32m+        if isinstance(branch, dict) and "enum" in branch[m
[32m+    )[m
[32m+[m
[32m+    assert enum_values == ["low", "medium", "high", "xhigh", "none"][m
[1;38mdiff --git a/tests/sdk/llm/test_model_features.py b/tests/sdk/llm/test_model_features.py[m
[1;38mindex 9c4bc929fa..5c9c7f16ec 100644[m
[1;38m--- a/tests/sdk/llm/test_model_features.py[m
[1;38m+++ b/tests/sdk/llm/test_model_features.py[m
@@ -8,6 +8,7 @@
     get_features,
     model_matches,
 )
[32m+from openhands.sdk.llm.utils.model_info import get_litellm_model_info[m
 
 
 @pytest.mark.parametrize(
@@ -65,20 +66,15 @@ def test_model_matches(name, pattern, expected):
         ("litellm_proxy/gpt-5", True),
         ("litellm_proxy/claude-opus-4-5", True),
         ("litellm_proxy/gemini-3-flash-preview", True),
[31m-        # LiteLLM recognizes Claude Fable 5 directly.[m
         ("claude-fable-5", True),
         ("anthropic/claude-fable-5", True),
         ("litellm_proxy/anthropic/claude-fable-5", True),
[31m-        # Kimi K3 always thinks and accepts top-level reasoning_effort, but the[m
[31m-        # pinned LiteLLM metadata does not recognize it yet.[m
         ("kimi-k3", True),
         ("moonshot/kimi-k3", True),
         ("litellm_proxy/moonshot/kimi-k3", True),
[31m-        # LiteLLM recognizes Opus 5 directly.[m
         ("claude-opus-5", True),
         ("anthropic/claude-opus-5", True),
         ("litellm_proxy/anthropic/claude-opus-5", True),
[31m-        # LiteLLM recognizes first-party and Bedrock Claude Opus 4.8 IDs.[m
         ("claude-opus-4-8", True),
         ("anthropic/claude-opus-4-8", True),
         ("bedrock/us.anthropic.claude-opus-4-8-v1:0", True),
@@ -276,6 +272,103 @@ def test_get_features_unknown_model():
     assert features.supports_stop_words is True  # Most models support stop words
 
 
[32m+def test_metadata_drives_adaptive_thinking_and_sampling():[m
[32m+    features = get_features([m
[32m+        "anthropic/claude-sonnet-5",[m
[32m+        model_info={[m
[32m+            "litellm_provider": "anthropic",[m
[32m+            "supports_reasoning": True,[m
[32m+            "supports_adaptive_thinking": True,[m
[32m+            "supports_sampling_params": False,[m
[32m+            "supports_prompt_caching": True,[m
[32m+        },[m
[32m+    )[m
[32m+[m
[32m+    assert features.supports_reasoning_effort is True[m
[32m+    assert features.thinking_mode == "adaptive"[m
[32m+    assert features.supports_extended_thinking is False[m
[32m+    assert features.supports_sampling_params is False[m
[32m+    assert features.supports_prompt_cache is True[m
[32m+[m
[32m+[m
[32m+def test_gemini_metadata_does_not_enable_explicit_prompt_cache():[m
[32m+    features = get_features([m
[32m+        "litellm_proxy/gemini-3.1-pro-preview",[m
[32m+        model_info={[m
[32m+            "key": "gemini-3.1-pro-preview",[m
[32m+            "litellm_provider": "vertex_ai",[m
[32m+            "supports_prompt_caching": True,[m
[32m+        },[m
[32m+    )[m
[32m+[m
[32m+    assert features.supports_prompt_cache is False[m
[32m+[m
[32m+[m
[32m+def test_sampling_support_uses_preserved_litellm_metadata():[m
[32m+    model_info = get_litellm_model_info([m
[32m+        secret_api_key=None,[m
[32m+        base_url=None,[m
[32m+        model="anthropic/claude-sonnet-5",[m
[32m+    )[m
[32m+    assert model_info is not None[m
[32m+[m
[32m+    features = get_features([m
[32m+        "anthropic/claude-sonnet-5",[m
[32m+        model_info=model_info,[m
[32m+    )[m
[32m+[m
[32m+    assert features.supports_sampling_params is False[m
[32m+[m
[32m+[m
[32m+def test_metadata_false_takes_precedence_over_name_fallbacks():[m
[32m+    features = get_features([m
[32m+        "openai/gpt-5",[m
[32m+        model_info={[m
[32m+            "supports_reasoning": False,[m
[32m+            "supports_prompt_caching": False,[m
[32m+            "supported_endpoints": [],[m
[32m+        },[m
[32m+    )[m
[32m+[m
[32m+    assert features.supports_reasoning_effort is False[m
[32m+    assert features.supports_prompt_cache is False[m
[32m+    assert features.supports_responses_api is False[m
[32m+[m
[32m+[m
[32m+def test_capability_overrides_take_precedence_over_metadata():[m
[32m+    features = get_features([m
[32m+        "proxy/future-model",[m
[32m+        model_info={[m
[32m+            "supports_reasoning": False,[m
[32m+            "supports_adaptive_thinking": False,[m
[32m+            "supports_sampling_params": True,[m
[32m+            "supports_vision": False,[m
[32m+        },[m
[32m+        overrides={[m
[32m+            "supports_reasoning_effort": True,[m
[32m+            "thinking_mode": "adaptive",[m
[32m+            "supports_sampling_params": False,[m
[32m+            "supports_responses_api": True,[m
[32m+            "supports_vision": True,[m
[32m+        },[m
[32m+    )[m
[32m+[m
[32m+    assert features.supports_reasoning_effort is True[m
[32m+    assert features.thinking_mode == "adaptive"[m
[32m+    assert features.supports_sampling_params is False[m
[32m+    assert features.supports_responses_api is True[m
[32m+    assert features.supports_vision is True[m
[32m+[m
[32m+[m
[32m+def test_responses_api_is_discovered_from_model_metadata():[m
[32m+    features = get_features([m
[32m+        "proxy/future-model",[m
[32m+        model_info={"supported_endpoints": ["/v1/chat/completions", "/v1/responses"]},[m
[32m+    )[m
[32m+[m
[32m+    assert features.supports_responses_api is True[m
[32m+[m
[32m+[m
 def test_get_features_empty_model():
     """Test get_features with empty or None model."""
     features_empty = get_features("")
[1;38mdiff --git a/tests/sdk/llm/test_model_info_proxy_lookup.py b/tests/sdk/llm/test_model_info_proxy_lookup.py[m
[1;38mindex 45fa838d16..11a83ff261 100644[m
[1;38m--- a/tests/sdk/llm/test_model_info_proxy_lookup.py[m
[1;38m+++ b/tests/sdk/llm/test_model_info_proxy_lookup.py[m
@@ -15,6 +15,7 @@
 
 from openhands.sdk.llm.utils.model_info import (
     _get_model_info_from_litellm_proxy,
[32m+    _merge_raw_model_metadata,[m
     get_litellm_model_info,
 )
 
@@ -120,3 +121,20 @@ def test_get_litellm_model_info_uses_proxy_for_openhands_provider_model():
         )
     assert info is not None
     assert info.get("supports_vision") is True
[32m+[m
[32m+[m
[32m+def test_raw_registry_capabilities_survive_typed_model_info_projection():[m
[32m+    raw = {[m
[32m+        "future-model": {[m
[32m+            "supports_adaptive_thinking": True,[m
[32m+            "supports_sampling_params": False,[m
[32m+        }[m
[32m+    }[m
[32m+    with patch.dict("openhands.sdk.llm.utils.model_info.model_cost", raw, clear=True):[m
[32m+        info = _merge_raw_model_metadata([m
[32m+            {"key": "future-model", "supports_reasoning": True}[m
[32m+        )[m
[32m+[m
[32m+    assert info["supports_reasoning"] is True[m
[32m+    assert info["supports_adaptive_thinking"] is True[m
[32m+    assert info["supports_sampling_params"] is False[m
[1;38mdiff --git a/tests/sdk/llm/test_responses_parsing_and_kwargs.py b/tests/sdk/llm/test_responses_parsing_and_kwargs.py[m
[1;38mindex 29cf651093..296d521412 100644[m
[1;38m--- a/tests/sdk/llm/test_responses_parsing_and_kwargs.py[m
[1;38m+++ b/tests/sdk/llm/test_responses_parsing_and_kwargs.py[m
@@ -78,8 +78,7 @@ def test_normalize_responses_kwargs_policy():
     out = select_responses_options(
         llm, {"temperature": 0.3}, include=["text.output_text"], store=None
     )
[31m-    # Temperature forced to 1.0 for Responses path[m
[31m-    assert out["temperature"] == 1.0[m
[32m+    assert out["temperature"] == 0.3[m
     assert out["tool_choice"] == "auto"
     # include should contain original and encrypted_content
     assert set(out["include"]) >= {"text.output_text", "reasoning.encrypted_content"}
@@ -93,6 +92,26 @@ def test_normalize_responses_kwargs_policy():
     assert out["max_output_tokens"] == 128
 
 
[32m+def test_responses_options_strip_sampling_when_metadata_rejects_it():[m
[32m+    llm = LLM([m
[32m+        model="proxy/future-responses-model",[m
[32m+        api_mode="responses",[m
[32m+        temperature=0.7,[m
[32m+        capability_overrides={"supports_sampling_params": False},[m
[32m+    )[m
[32m+[m
[32m+    out = select_responses_options([m
[32m+        llm,[m
[32m+        {"temperature": 0.3, "top_p": 0.8, "top_k": 20},[m
[32m+        include=None,[m
[32m+        store=None,[m
[32m+    )[m
[32m+[m
[32m+    assert "temperature" not in out[m
[32m+    assert "top_p" not in out[m
[32m+    assert "top_k" not in out[m
[32m+[m
[32m+[m
 def test_normalize_responses_kwargs_with_summary():
     """Test reasoning_summary is included when set (verified orgs)."""
     llm = LLM(model="gpt-5-mini", reasoning_effort="high", reasoning_summary="detailed")
[1;38mdiff --git a/tests/sdk/llm/test_subscription_mode.py b/tests/sdk/llm/test_subscription_mode.py[m
[1;38mindex e876751cc9..dc27f7731d 100644[m
[1;38m--- a/tests/sdk/llm/test_subscription_mode.py[m
[1;38m+++ b/tests/sdk/llm/test_subscription_mode.py[m
@@ -105,7 +105,6 @@ def test_subscription_skips_unsupported_param(param: str):
     "param,expected_value",
     [
         ("prompt_cache_retention", "24h"),
[31m-        ("temperature", 1.0),[m
     ],
 )
 def test_non_subscription_keeps_scalar_param(param: str, expected_value: Any):
@@ -117,6 +116,14 @@ def test_non_subscription_keeps_scalar_param(param: str, expected_value: Any):
     assert opts.get(param) == expected_value
 
 
[32m+def test_non_subscription_does_not_invent_temperature():[m
[32m+    llm = LLM(model="openai/gpt-5.2-codex", reasoning_effort="high")[m
[32m+[m
[32m+    opts = select_responses_options(llm, {}, include=None, store=None)[m
[32m+[m
[32m+    assert "temperature" not in opts[m
[32m+[m
[32m+[m
 @pytest.mark.parametrize(
     "param,check",
     [
[?2004h
[The command completed with exit code 0.]
[Current working directory: /var/lib/agent-canvas/.openhands/agent-canvas/workspaces/automation-runs/9fc35a79-ef17-46d8-965e-b79abaef8730/software-agent-sdk]
[Python interpreter: /var/lib/agent-canvas/.cache/uv/archive-v0/g9v2-voyPRjeMpCI/bin/python]
[Command finished with exit code 0]