Skip to content

Commit 77ee8a7

Browse files
fix(sdk/llm): emit output_config at top level for adaptive thinking
1 parent c93e689 commit 77ee8a7

2 files changed

Lines changed: 12 additions & 18 deletions

File tree

‎openhands-sdk/openhands/sdk/llm/options/chat_options.py‎

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -95,22 +95,18 @@ def select_chat_options(
9595
out["extra_body"] = llm.litellm_extra_body
9696

9797
# Opt-in adaptive thinking for models that only accept thinking.type="adaptive"
98-
# (Opus 4.7/4.8/Fable 5). Without display="summarized" the model's default is
99-
# "omitted" and thinking blocks come back empty, so display is required here.
100-
# output_config goes through extra_body so it survives older litellm releases
101-
# that don't recognize it as a top-level kwarg. Placed after the user-provided
102-
# extra_body pass-through so output_config merges into (not replaces) it.
98+
# (Opus 4.7/4.8/Fable 5). display="summarized" is REQUIRED for these models to
99+
# return populated thinking content (their default is "omitted"). output_config
100+
# is emitted at the top level so litellm transformations preserve it; an
101+
# earlier draft wrapped it in extra_body, but older litellm versions did not
102+
# unpack extra_body for Vertex Claude and the API rejected the request.
103103
if (
104104
llm.force_adaptive_thinking
105105
and get_features(llm.model).supports_adaptive_thinking
106106
):
107107
out["thinking"] = {"type": "adaptive", "display": "summarized"}
108108
if llm.reasoning_effort is not None and llm.reasoning_effort != "none":
109-
existing_eb = out.get("extra_body") or {}
110-
out["extra_body"] = {
111-
**existing_eb,
112-
"output_config": {"effort": llm.reasoning_effort},
113-
}
109+
out["output_config"] = {"effort": llm.reasoning_effort}
114110
# Strip reasoning_effort so litellm does not re-derive and overwrite
115111
# the thinking block via its reasoning_effort -> thinking mapping.
116112
out.pop("reasoning_effort", None)

‎tests/sdk/llm/test_chat_options.py‎

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -177,15 +177,15 @@ def test_force_adaptive_thinking_emits_adaptive_schema_for_opus_4_8():
177177
out = select_chat_options(llm, user_kwargs={}, has_tools=True)
178178

179179
assert out.get("thinking") == {"type": "adaptive", "display": "summarized"}
180-
assert out.get("extra_body") == {"output_config": {"effort": "high"}}
180+
assert out.get("output_config") == {"effort": "high"}
181181
assert out.get("max_tokens") == 4096
182182
assert "reasoning_effort" not in out
183183
assert "temperature" not in out
184184
assert "top_p" not in out
185185
assert "top_k" not in out
186186

187187

188-
def test_force_adaptive_thinking_merges_into_existing_extra_body():
188+
def test_force_adaptive_thinking_preserves_existing_extra_body():
189189
llm = DummyLLM(
190190
model="claude-opus-4-7",
191191
reasoning_effort="medium",
@@ -194,10 +194,8 @@ def test_force_adaptive_thinking_merges_into_existing_extra_body():
194194
)
195195
out = select_chat_options(llm, user_kwargs={}, has_tools=True)
196196

197-
assert out.get("extra_body") == {
198-
"return_token_ids": True,
199-
"output_config": {"effort": "medium"},
200-
}
197+
assert out.get("extra_body") == {"return_token_ids": True}
198+
assert out.get("output_config") == {"effort": "medium"}
201199

202200

203201
def test_force_adaptive_thinking_noop_when_model_not_supported():
@@ -209,7 +207,7 @@ def test_force_adaptive_thinking_noop_when_model_not_supported():
209207
out = select_chat_options(llm, user_kwargs={}, has_tools=True)
210208

211209
assert "thinking" not in out
212-
assert "output_config" not in (out.get("extra_body") or {})
210+
assert "output_config" not in out
213211
assert out.get("reasoning_effort") == "high"
214212

215213

@@ -222,4 +220,4 @@ def test_force_adaptive_thinking_disabled_by_default_for_opus_4_8():
222220

223221
assert "thinking" not in out
224222
assert out.get("reasoning_effort") == "high"
225-
assert "output_config" not in (out.get("extra_body") or {})
223+
assert "output_config" not in out

0 commit comments

Comments
 (0)