Skip to content

feat(llm): support user-configurable send_reasoning_content per model - #4446

Open
cheney32 wants to merge 2 commits into
OpenHands:mainfrom
cheney32:feat/send-reasoning-content-capability-override
Open

cheney32 wants to merge 2 commits into
OpenHands:mainfrom
cheney32:feat/send-reasoning-content-capability-override

Conversation

@cheney32

@cheney32 cheney32 commented Aug 10, 2026

Copy link
Copy Markdown

Let users opt a model in/out via LLM.capability_overrides instead of only the hardcoded allow-list, which stays as the fallback.

HUMAN:

This change makes it possible for users to control, per LLM instance, whether the SDK sends a model's full reasoning content back in the message input. Previously this was decided only by a hardcoded allow-list (SEND_REASONING_CONTENT_MODELS), so enabling a new thinking-capable model — or one served under a custom/proxy name — meant editing that list, releasing a new SDK version, and waiting for consumers to upgrade, with no way to turn it off for a model already on the list.

The fix routes send_reasoning_content through the same resolution path already used by other model capabilities, so an explicit LLM.capability_overrides value wins first, then LiteLLM metadata, and finally the hardcoded list as a fallback. It is fully backward compatible (with no override, the list behaves exactly as before) and adds no new public API or settings migration, since capability_overrides is an existing field.


AGENT:

Why

Whether the SDK sends full reasoning content back in the message input was
gated only by the hardcoded SEND_REASONING_CONTENT_MODELS allow-list in
openhands-sdk/openhands/sdk/llm/utils/model_features.py. Adding a new
thinking-capable model (or one behind a custom/proxy name) required editing the
list, releasing the SDK, and having consumers upgrade — and there was no way to
force-disable it for a listed model. This makes the behavior user-configurable
per LLM instance while keeping the list as a fallback.

Summary

  • Route send_reasoning_content through the existing _resolved_bool helper in
    get_features, so LLM.capability_overrides["send_reasoning_content"] takes
    precedence, then LiteLLM metadata, then the hardcoded list fallback.
  • Document send_reasoning_content as a supported key in the
    LLM.capability_overrides field description.
  • Add a unit test covering enabling a non-listed model and disabling a listed
    model via the override.

How to Test

uv run pytest tests/sdk/llm/test_model_features.py -k send_reasoning

Or programmatically:

from openhands.sdk.llm.utils.model_features import get_features

# fallback list still works
assert get_features("kimi-k2-thinking").send_reasoning_content is True
assert get_features("gpt-4o").send_reasoning_content is False
# per-instance override, both directions
assert get_features(
    "gpt-4o", overrides={"send_reasoning_content": True}
).send_reasoning_content is True
assert get_features(
    "kimi-k2-thinking", overrides={"send_reasoning_content": False}
).send_reasoning_content is False

Design Doc

See .pr/design.md for the before/after and rationale.

Type

  • Bug fix
  • Feature
  • Refactor
  • Breaking change
  • Docs / chore

Notes

  • Backward compatible: with no override, the hardcoded list decides exactly as
    before.
  • No new public API surface and no persisted-settings migration:
    capability_overrides is an existing dict[str, bool | str] field.

Jev-Fast-Audit

Jev fast audit · estimates · 0.43s · commit 4169c8c
Strongest signal: No primary concern selected.
Evidence: No primary concern to locate.
Coverage: complete supplied coverage; 4/4 hunks, 4/4 files.

All estimates and evidence
Estimate Likelihood / value Direct evidence
SQL injection 2.0% No direct hunk selected
Command injection 3.0% No direct hunk selected
Weakened authentication 3.0% No direct hunk selected
Weakened authorization 5.0% No direct hunk selected
Contract regression 7.0% No direct hunk selected
Data loss 3.0% No direct hunk selected
Sensitive data disclosure 4.0% No direct hunk selected
Unexpected data transfer 3.0% No direct hunk selected
Credential misuse 4.0% No direct hunk selected
Untrusted instruction authority 3.0% No direct hunk selected
Package source redirection 3.0% No direct hunk selected
Unverified remote execution 2.0% No direct hunk selected
Privileged environment access 2.0% No direct hunk selected
Security assessment bypass 8.0% No direct hunk selected
Prohibited workload 2.0% No direct hunk selected
Primary concern None selected; confidence 64.0% No primary concern to locate

Let users opt a model in/out via LLM.capability_overrides instead of
only the hardcoded allow-list, which stays as the fallback.

Co-authored-by: openhands <openhands@all-hands.dev>
@github-actions

Copy link
Copy Markdown
Contributor

📁 PR Artifacts Notice

This PR contains a .pr/ directory with temporary PR-specific documents. Because this is a fork PR, the directory will be automatically removed from main immediately after merge.

@enyst enyst left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thank you for the proposal. A little question, is there an issue showing that this is a problem?

On another note, I’m afraid this variable is misnamed: many other LLMs have their reasoning content sent back to them, in the SDK.

This one only means that LiteLLM doesn’t consider correctly some open LLMs as reasoning models, so we can’t recognize them together with the rest, which is why we recognize them here. Or so I recall, open to change my opinion if that’s not the case.

I think maybe we could rename it first, just so we know better… wdyt?

@cheney32

Copy link
Copy Markdown
Author

Thank you for the proposal. A little question, is there an issue showing that this is a problem?

On another note, I’m afraid this variable is misnamed: many other LLMs have their reasoning content sent back to them, in the SDK.

This one only means that LiteLLM doesn’t consider correctly some open LLMs as reasoning models, so we can’t recognize them together with the rest, which is why we recognize them here. Or so I recall, open to change my opinion if that’s not the case.

I think maybe we could rename it first, just so we know better… wdyt?

Thanks for the reply!

Related discussion
#1343
#1464

The concern applies to both self-deployed models and proxied models: they usually run behind a custom/aliased model name, and both of our recognition paths key off a known model name — which is exactly where they fall through.

LiteLLM recognition: it relies on LiteLLM's model registry. A self-hosted or proxied model behind a custom name isn't in that registry — we literally get 『This model isn't mapped yet. model=qwen3.6-35b, custom_llm_provider=openai.』, so LiteLLM can't classify it (capabilities, cost, reasoning) at all.

The SEND_REASONING_CONTENT_MODELS fallback: it's a substring match on the model name, so it only helps when the operator happens to name the endpoint with one of the listed substrings — which callers often can't control.
So for a large number of self-deployed and proxied reasoning models, neither LiteLLM recognition nor the hardcoded list matches. The reasoning_content then gets dropped on the next turn, the model loses its own prior thinking, and today the only workarounds are editing the list + cutting an SDK release + having every consumer upgrade — with no way to force-disable it for a listed model either. This affects a large portion of self-hosted reasoning setups, not just a rare edge case.

That's the gap this change fills: a per-instance escape hatch (via capability_overrides) for exactly the models automatic detection and the static list cannot know about, while leaving the default (list-based) behavior untouched.
On the naming — you're right that it's misleading, and we'd rather you decide the final name. To get the ball rolling, some candidates depending on which angle you prefer:

  • Recognition-oriented (matches your framing that this is really "reasoning models LiteLLM doesn't detect"): EXTRA_REASONING_MODELS, REASONING_MODELS_UNDETECTED_BY_LITELLM.

  • Behavior-oriented (what the flag actually does in the Chat Completions path): ECHO_REASONING_CONTENT, REQUIRES_REASONING_CONTENT_ECHO.

Do you have a naming you'd prefer?

@github-actions

Copy link
Copy Markdown
Contributor

This PR is stale because it has been open for 40 days with no activity. Remove the stale label or leave a comment, otherwise it will be closed in 10 days.

@github-actions github-actions Bot added the Stale label Sep 20, 2026
@all-hands-bot

Copy link
Copy Markdown
Collaborator

🚦 CI is currently failing on this PR's latest commit.

Please fix the failing checks before OpenHands reviews it - this is re-checked automatically once you push a new commit. (A maintainer can also request @all-hands-bot as a reviewer to have it reviewed regardless of CI status.)

This is an automated check - no AI was used to generate this comment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants