OpenHands / OpenHands/software-agent-sdk
[Bug]: DeepSeek V4.x reasoning_effort and output cap depend on proxy metadata (dropped without it, 384K default cap with it; openai/ ignores overrides)
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 1.1k
- Forks
- 542
- Avg merge
- 1d 19h
- Merged PRs (30d)
- 137
Description
Is there an existing issue for the same bug?
- I have searched existing issues and this is not a duplicate.
Related:
- #4934: stop sending
reasoning_effortbased on genericsupports_reasoning. It interacts with (1) below. - #4941:
deepseek-v4.1-flashbecame the default Cloud model. - #3317: the effective max output tokens exceed real provider limits. It relates to (2).
Bug Description
The SDK derives reasoning_effort support and the default output cap for DeepSeek V4.x from proxy or LiteLLM metadata. The result depends on whether that metadata exists, and it is wrong either way:
- Without proxy metadata,
reasoning_effortis dropped foropenhands/deepseek-v4.1-flashandlitellm_proxy/deepseek-v4.1-flash.get_supported_openai_paramsreturns nothing for bare DeepSeek ids, and LiteLLM 1.93 has no registry entry fordeepseek-v4.1-flash, so support falls back toFalse.deepseek-v4-flashand-v4-proonly work because LiteLLM's registry marks themsupports_reasoning: true.- A proxy alias with
model_info(like OpenHands' hosted proxy) masks the problem. It shows up on proxies without the alias, or when/v1/model/infofails. The fix proposed in #4934 would expose it for v4-flash, v4-pro and v4.1-flash everywhere.
- With proxy metadata, the SDK sends
max_completion_tokensequal to the model's advertised maximum (e.g.max_output_tokens: 384000) on every request.- On OpenRouter, that silently excludes endpoints with lower limits, so provider pins configured on the proxy stop working.
- Example: an alias pinning
provider.order: ["Baseten"](limit 32,768) falls back to other providers.
- On
openai/…routes,capability_overrides={"supports_reasoning_effort": True}has no effect. LiteLLM's defaultdrop_params=Trueremoves the parameter for OpenAI-compatible endpoints.
Expected Behavior
reasoning_effortis sent for DeepSeek V4.x onopenhands/andlitellm_proxy/routes without relying on proxy metadata or genericsupports_reasoning.- The SDK does not send, by default, an output cap larger than the route can serve.
- When
capability_overridesenablesreasoning_effort, the parameter reaches the request body (e.g. viaallowed_openai_params).
Actual Behavior
Offline repro against a fake OpenAI-compatible server with no model metadata (script below), on main @ 28e8ed273 with LiteLLM 1.93.0:
$ uv run python repro_wire.py
openhands/deepseek-v4-flash overrides=None -> reasoning_effort on the wire: 'high'
openhands/deepseek-v4.1-flash overrides=None -> reasoning_effort on the wire: None
litellm_proxy/deepseek-v4.1-flash overrides=None -> reasoning_effort on the wire: None
openai/openrouter/deepseek/deepseek-v4.1-flash overrides=None -> reasoning_effort on the wire: None
openai/openrouter/deepseek/deepseek-v4.1-flash overrides={'supports_reasoning_effort': True} -> reasoning_effort on the wire: None
For (2), the probe went through a LiteLLM proxy to OpenRouter's deepseek/deepseek-v4.1-flash:
provider |
max_completion_tokens |
Result |
|---|---|---|
order: ["Baseten"], no fallbacks |
not set | served by Baseten |
order: ["Baseten"], no fallbacks |
384000 | 404 "No endpoints found" |
order: [..., "Baseten"], with fallbacks |
384000 | served by another provider (Morph) |
Steps to Reproduce
- Save the script below as
repro_wire.pyin a checkout ofmain. uv run python repro_wire.py- For (2), send the requests in the table through any LiteLLM proxy that has an OpenRouter key.
Acceptance Criteria
-
openhands/deepseek-v4.1-flashandlitellm_proxy/deepseek-v4.1-flashsendreasoning_effortwith no proxy metadata (the repro prints'high'). -
deepseek-v4-flashand-v4-prokeep sending it without depending on genericsupports_reasoning, so the fix composes with #4934. - With metadata advertising
max_output_tokens: 384000, the SDK does not send that value as a default cap, unless the user configured it. -
openai/…pluscapability_overrides={"supports_reasoning_effort": True}sendsreasoning_effort. Behavior without the override is unchanged. -
reasoning_effortis still not sent where support is unknown or negative (no regression of #4934). - Unit tests cover the no-metadata, with-metadata and
openai/-with-override cases.
Installation Method
uv (repository checkout)
SDK Version
1.49.2 (main @ 28e8ed273)
Version Confirmation
- I have confirmed this bug exists on the LATEST version of OpenHands SDK
Python Version
3.12
Model Name (if applicable)
deepseek-v4.1-flash (openhands/, litellm_proxy/, openai/openrouter/…)
Operating System
MacOS
Logs and Error Messages
Nothing is logged; the parameter is silently absent. Calling LiteLLM directly with drop_params=False shows the drop on the openai/ route:
litellm.UnsupportedParamsError: openai does not support parameters: ['reasoning_effort'], for model=openrouter/deepseek/deepseek-v4.1-flash.
... If you want to use these params dynamically send allowed_openai_params=['reasoning_effort'] in your request.
Minimal Code Sample
import json
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
from openhands.sdk import LLM
from openhands.sdk.llm import Message, TextContent
bodies = []
class FakeOpenAICompatibleServer(BaseHTTPRequestHandler):
def do_GET(self): # /v1/model/info probe: no metadata
self._send({"data": []})
def do_POST(self):
bodies.append(json.loads(self.rfile.read(int(self.headers["Content-Length"]))))
self._send({"id": "x", "object": "chat.completion", "created": 1, "model": "m",
"choices": [{"index": 0, "message": {"role": "assistant", "content": "ok"}, "finish_reason": "stop"}],
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}})
def _send(self, payload):
data = json.dumps(payload).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def log_message(self, *args):
pass
server = HTTPServer(("127.0.0.1", 0), FakeOpenAICompatibleServer)
threading.Thread(target=server.serve_forever, daemon=True).start()
cases = [
("openhands/deepseek-v4-flash", None),
("openhands/deepseek-v4.1-flash", None),
("litellm_proxy/deepseek-v4.1-flash", None),
("openai/openrouter/deepseek/deepseek-v4.1-flash", None),
("openai/openrouter/deepseek/deepseek-v4.1-flash", {"supports_reasoning_effort": True}),
]
for model, overrides in cases:
llm = LLM(model=model, api_key="x", base_url=f"http://127.0.0.1:{server.server_port}/v1",
reasoning_effort="high", capability_overrides=overrides or {}, usage_id=model + str(overrides))
llm.completion([Message(role="user", content=[TextContent(text="hi")])])
print(f"{model:50} overrides={overrides} -> reasoning_effort on the wire: {bodies[-1].get('reasoning_effort')!r}")
Screenshots and Additional Context
- DeepSeek documents the parameter.
reasoning_efforttakesnone/low/high/max, defaulthigh(docs). In pinned-provider probes (DeepSeek, DeepInfra),nonegave 0 reasoning tokens, and leaving it unset behaved likehigh. So with the defaulthighthe loss is invisible, but any other level is silently ignored. - Workaround:
litellm_proxy/openrouter/deepseek/deepseek-v4.1-flashresolves support and sendsreasoning_effort. Setmax_output_tokensexplicitly to avoid the advertised 384,000 cap. - Out of scope: LiteLLM's native
deepseek/provider maps effort to thinking on/off, which is an upstream LiteLLM behavior.
Filed by an AI agent (Claude Code) on behalf of @simonrosenberg.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by saving and running the provided repro_wire.py script, then trace the LLM construction and completion path for capability detection and request parameters. Use the repro cases and add the requested unit coverage for no metadata, metadata, and openai/ overrides. Done means the acceptance criteria pass without sending an unsafe default output cap or unsupported reasoning_effort.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- api, backend-api-design
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100