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)

Open
#5,181 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug llm priority:high ready-for-dev
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_effort based on generic supports_reasoning. It interacts with (1) below.
  • #4941: deepseek-v4.1-flash became 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:

  1. Without proxy metadata, reasoning_effort is dropped for openhands/deepseek-v4.1-flash and litellm_proxy/deepseek-v4.1-flash.
    • get_supported_openai_params returns nothing for bare DeepSeek ids, and LiteLLM 1.93 has no registry entry for deepseek-v4.1-flash, so support falls back to False.
    • deepseek-v4-flash and -v4-pro only work because LiteLLM's registry marks them supports_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/info fails. The fix proposed in #4934 would expose it for v4-flash, v4-pro and v4.1-flash everywhere.
  2. With proxy metadata, the SDK sends max_completion_tokens equal 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.
  3. On openai/… routes, capability_overrides={"supports_reasoning_effort": True} has no effect. LiteLLM's default drop_params=True removes the parameter for OpenAI-compatible endpoints.
Expected Behavior
  • reasoning_effort is sent for DeepSeek V4.x on openhands/ and litellm_proxy/ routes without relying on proxy metadata or generic supports_reasoning.
  • The SDK does not send, by default, an output cap larger than the route can serve.
  • When capability_overrides enables reasoning_effort, the parameter reaches the request body (e.g. via allowed_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
  1. Save the script below as repro_wire.py in a checkout of main.
  2. uv run python repro_wire.py
  3. For (2), send the requests in the table through any LiteLLM proxy that has an OpenRouter key.
Acceptance Criteria
  • openhands/deepseek-v4.1-flash and litellm_proxy/deepseek-v4.1-flash send reasoning_effort with no proxy metadata (the repro prints 'high').
  • deepseek-v4-flash and -v4-pro keep sending it without depending on generic supports_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/… plus capability_overrides={"supports_reasoning_effort": True} sends reasoning_effort. Behavior without the override is unchanged.
  • reasoning_effort is 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_effort takes none/low/high/max, default high (docs). In pinned-provider probes (DeepSeek, DeepInfra), none gave 0 reasoning tokens, and leaving it unset behaved like high. So with the default high the loss is invisible, but any other level is silently ignored.
  • Workaround: litellm_proxy/openrouter/deepseek/deepseek-v4.1-flash resolves support and sends reasoning_effort. Set max_output_tokens explicitly 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.