@@ -39,6 +39,77 @@ def _env(name: str) -> str | None:
3939 return str(value).strip()
4040
4141
42+ def _install_send_reasoning_content_override(model: str) -> None:
43+ """Force OpenHands SDK to preserve/send reasoning content for this model."""
44+ try:
45+ from openhands.sdk.llm.utils import model_features
46+
47+ tokens = [model]
48+ if "/" in model:
49+ tokens.append(model.split("/", 1)[-1])
50+ for token in tokens:
51+ if token and token not in model_features.SEND_REASONING_CONTENT_MODELS:
52+ model_features.SEND_REASONING_CONTENT_MODELS.append(token)
53+
54+ cache_clear = getattr(model_features.get_features, "cache_clear", None)
55+ if cache_clear:
56+ cache_clear()
57+ except Exception as exc:
58+ print(
59+ f"Warning: failed to enable reasoning-content send override: {exc}",
60+ file=sys.stderr,
61+ )
62+
63+ try:
64+ from openhands.sdk.llm.message import Message
65+
66+ if getattr(Message, "_featurebench_reasoning_alias_patch", False):
67+ return
68+
69+ original = Message.from_llm_chat_message.__func__
70+
71+ class _ReasoningContentProxy:
72+ def __init__(self, wrapped: Any, reasoning_content: str):
73+ self._wrapped = wrapped
74+ self.reasoning_content = reasoning_content
75+
76+ def __getattr__(self, name: str) -> Any:
77+ return getattr(self._wrapped, name)
78+
79+ def _extract_reasoning_content(message: Any) -> str | None:
80+ reasoning = getattr(message, "reasoning_content", None)
81+ if reasoning:
82+ return str(reasoning)
83+
84+ reasoning = getattr(message, "reasoning", None)
85+ if reasoning:
86+ return str(reasoning)
87+
88+ provider_fields = getattr(message, "provider_specific_fields", None)
89+ if isinstance(provider_fields, dict):
90+ for key in ("reasoning_content", "reasoning"):
91+ reasoning = provider_fields.get(key)
92+ if reasoning:
93+ return str(reasoning)
94+
95+ return None
96+
97+ def patched(cls: type[Message], message: Any) -> Message:
98+ if getattr(message, "reasoning_content", None) is None:
99+ reasoning_content = _extract_reasoning_content(message)
100+ if reasoning_content:
101+ message = _ReasoningContentProxy(message, reasoning_content)
102+ return original(cls, message)
103+
104+ Message.from_llm_chat_message = classmethod(patched)
105+ setattr(Message, "_featurebench_reasoning_alias_patch", True)
106+ except Exception as exc:
107+ print(
108+ f"Warning: failed to install reasoning field alias patch: {exc}",
109+ file=sys.stderr,
110+ )
111+
112+
42113def _event_data(event: Any) -> dict[str, Any]:
43114 try:
44115 return event.model_dump(mode="json", exclude_none=True)
@@ -128,6 +199,9 @@ def _build_llm() -> Any:
128199 if not model:
129200 raise RuntimeError("LLM_MODEL is required for OpenHands SDK runner.")
130201
202+ if _truthy(_env("LLM_SEND_REASONING_CONTENT")):
203+ _install_send_reasoning_content_override(model)
204+
131205 kwargs: dict[str, Any] = {"model": model}
132206
133207 api_key = _env("LLM_API_KEY")
@@ -287,18 +361,8 @@ def install_script(self) -> str:
287361
288362export PIP_CACHE_DIR="$CACHE_ROOT/pip"
289363export UV_CACHE_DIR="$CACHE_ROOT/uv"
290-
291- # If a local uv Python mirror exists and is non-empty, use it. Otherwise, let uv download Python from the default upstream sources.
292364UV_PYTHON_MIRROR_DIR="$CACHE_ROOT/uv/python-mirror"
293- if [ -z "${{UV_PYTHON_INSTALL_MIRROR:-}}" ]; then
294- if [ -d "$UV_PYTHON_MIRROR_DIR" ] && [ "$(ls -A "$UV_PYTHON_MIRROR_DIR" 2>/dev/null)" ]; then
295- export UV_PYTHON_INSTALL_MIRROR="file://$UV_PYTHON_MIRROR_DIR"
296- echo "Using local uv python mirror: $UV_PYTHON_INSTALL_MIRROR"
297- else
298- unset UV_PYTHON_INSTALL_MIRROR
299- echo "Local uv python mirror is empty; using upstream python downloads"
300- fi
301- fi
365+ PYTHON_INSTALL_MIRROR="${{UV_PYTHON_INSTALL_MIRROR:-https://ghfast.top/https://github.com/astral-sh/python-build-standalone/releases/download}}"
302366
303367UV_DIR="/opt/featurebench/uv"
304368UV_BIN_PRIMARY="$UV_DIR/bin/uv"
@@ -338,8 +402,8 @@ def install_script(self) -> str:
338402
339403# Configure uv index mirror (TUNA)
340404mkdir -p ~/.config/uv
341- cat > ~/.config/uv/uv.toml <<' EOF'
342- python-install-mirror = "https://ghfast.top/https://github.com/astral-sh/python-build-standalone/releases/download "
405+ cat > ~/.config/uv/uv.toml <<EOF
406+ python-install-mirror = "$PYTHON_INSTALL_MIRROR "
343407[[index]]
344408url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/"
345409default = true
@@ -356,7 +420,54 @@ def install_script(self) -> str:
356420 "$UV_BIN" pip install --index-url "$FALLBACK_INDEX_URL" "$@"
357421}}
358422
359- # Install Python via uv (downloads cached via UV_CACHE_DIR)
423+ cache_uv_python_download() {{
424+ if [[ "$PYTHON_INSTALL_MIRROR" == file://* ]]; then
425+ export UV_PYTHON_INSTALL_MIRROR="$PYTHON_INSTALL_MIRROR"
426+ echo "Using configured uv python mirror: $UV_PYTHON_INSTALL_MIRROR"
427+ return 0
428+ fi
429+
430+ local download_url rel_path target_path part_path
431+ download_url="$("$UV_BIN" python list "$PY_VERSION" --only-downloads --show-urls | awk 'NR == 1 {{print $2}}')"
432+ if [ -z "$download_url" ] || [[ "$download_url" == "<"* ]]; then
433+ echo "Unable to resolve uv Python download URL for $PY_VERSION" >&2
434+ return 1
435+ fi
436+ if [[ "$download_url" != "$PYTHON_INSTALL_MIRROR/"* ]]; then
437+ echo "Resolved uv Python URL does not use configured mirror: $download_url" >&2
438+ return 1
439+ fi
440+
441+ rel_path="${{download_url#"$PYTHON_INSTALL_MIRROR"/}}"
442+ rel_path="${{rel_path//%2B/+}}"
443+ rel_path="${{rel_path//%2b/+}}"
444+ target_path="$UV_PYTHON_MIRROR_DIR/$rel_path"
445+ part_path="$target_path.part"
446+
447+ mkdir -p "$(dirname "$target_path")"
448+ if [ -s "$target_path" ] && tar -tzf "$target_path" >/dev/null 2>&1; then
449+ echo "Using cached uv Python archive: $target_path"
450+ else
451+ if [ -s "$target_path" ]; then
452+ echo "Cached uv Python archive is invalid; redownloading: $target_path" >&2
453+ rm -f "$target_path"
454+ fi
455+ echo "Caching uv Python archive: $download_url -> $target_path"
456+ curl -fL --retry 3 --retry-delay 2 --connect-timeout 20 -C - -o "$part_path" "$download_url"
457+ if ! tar -tzf "$part_path" >/dev/null 2>&1; then
458+ echo "Downloaded uv Python archive is invalid: $part_path" >&2
459+ rm -f "$part_path"
460+ return 1
461+ fi
462+ mv "$part_path" "$target_path"
463+ fi
464+
465+ export UV_PYTHON_INSTALL_MIRROR="file://$UV_PYTHON_MIRROR_DIR"
466+ echo "Using local uv python mirror: $UV_PYTHON_INSTALL_MIRROR"
467+ }}
468+
469+ # Install Python via uv. The archive is cached in a local mirror first so future containers reuse it.
470+ cache_uv_python_download
360471$UV_BIN python install $PY_VERSION
361472
362473# Create venv (container-local)
@@ -402,7 +513,13 @@ def get_run_command(self, instruction: str) -> str:
402513 "if [ ! -x /opt/openhands-venv/bin/python ]; then "
403514 "echo '/opt/openhands-venv/bin/python not found' >&2; exit 127; "
404515 "fi; "
405- "if /opt/openhands-venv/bin/python -c "
516+ "if [[ \" ${LLM_SEND_REASONING_CONTENT,,}\" =~ ^(1|true|yes|on)$ ]] && "
517+ "/opt/openhands-venv/bin/python -c "
518+ "\" import importlib.util, sys; "
519+ "sys.exit(0 if importlib.util.find_spec('openhands.sdk') else 1)\" ; "
520+ "then "
521+ f"/opt/openhands-venv/bin/python /agent-logs/openhands-sdk-runner.py --task-file { task_file } ; "
522+ "elif /opt/openhands-venv/bin/python -c "
406523 "\" import importlib.util, sys; "
407524 "sys.exit(0 if importlib.util.find_spec('openhands.core.main') else 1)\" ; "
408525 "then "
@@ -445,6 +562,8 @@ def get_env_setup_script(self) -> str:
445562 "LLM_REASONING_EFFORT" : self .env_vars .get ("LLM_REASONING_EFFORT" ),
446563 # Force native tool calling (OpenHands LLMConfig.native_tool_calling via LLM_ env mapping)
447564 "LLM_NATIVE_TOOL_CALLING" : self .env_vars .get ("LLM_NATIVE_TOOL_CALLING" ),
565+ # Force OpenHands SDK to send prior assistant reasoning_content in history.
566+ "LLM_SEND_REASONING_CONTENT" : self .env_vars .get ("LLM_SEND_REASONING_CONTENT" ),
448567 # Disable features not needed for FeatureBench
449568 "AGENT_ENABLE_PROMPT_EXTENSIONS" : "false" ,
450569 "AGENT_ENABLE_BROWSING" : "false" ,
0 commit comments