fix(#4146): read reasoning_content gateway chunks

This commit is contained in:
Rod Boev 2026-06-14 10:22:43 -04:00
parent 42e85813a3
commit 769fd11f22
2 changed files with 83 additions and 0 deletions

View file

@ -131,6 +131,24 @@ def _gateway_sse_delta(payload: dict) -> str:
return ""
def _gateway_sse_reasoning_delta(payload: dict) -> str:
"""Extract reasoning text from OpenAI-compatible streaming chunks."""
try:
choices = payload.get("choices") or []
if not choices:
return ""
choice = choices[0] or {}
delta = choice.get("delta") or {}
reasoning = delta.get("reasoning_content")
if isinstance(reasoning, str) and reasoning.strip():
return reasoning
message = choice.get("message") or {}
reasoning = message.get("reasoning_content")
return reasoning if isinstance(reasoning, str) and reasoning.strip() else ""
except Exception:
return ""
def _gateway_stream_usage(payload: dict) -> dict:
usage = payload.get("usage") if isinstance(payload, dict) else None
if not isinstance(usage, dict):
@ -406,6 +424,11 @@ def _run_gateway_chat_streaming(
sse_event = "message"
continue
last_payload = payload
reasoning_delta = _gateway_sse_reasoning_delta(payload)
if reasoning_delta:
if stream_id in STREAM_REASONING_TEXT:
STREAM_REASONING_TEXT[stream_id] += reasoning_delta
put_gateway_event("reasoning", {"text": reasoning_delta})
delta = _gateway_sse_delta(payload)
if delta:
final_text += delta

View file

@ -15,6 +15,7 @@ from api.gateway_chat import (
_gateway_http_error_event,
_gateway_reasoning_delta,
_gateway_sse_delta,
_gateway_sse_reasoning_delta,
_gateway_stream_usage,
_gateway_tool_progress_event,
gateway_chat_config_status,
@ -157,6 +158,12 @@ def test_gateway_reasoning_delta_keeps_string_deltas_and_ignores_structured_payl
assert _gateway_reasoning_delta({"text": ["safe"], "preview": " more"}) == " more"
def test_gateway_sse_reasoning_delta_extracts_reasoning_content_chunks():
assert _gateway_sse_reasoning_delta({"choices": [{"delta": {"reasoning_content": "Let me"}}]}) == "Let me"
assert _gateway_sse_reasoning_delta({"choices": [{"message": {"reasoning_content": "Done thinking"}}]}) == "Done thinking"
assert _gateway_sse_reasoning_delta({"choices": [{"delta": {"reasoning_content": " "}}]}) == ""
def test_gateway_http_401_reports_gateway_auth_not_provider_key():
exc = urllib.error.HTTPError(
"http://gateway.local/v1/chat/completions",
@ -427,6 +434,59 @@ def test_gateway_chat_worker_preserves_reasoning_delta_whitespace_and_persists_r
assert not any("debug" in text for text in reasoning_events)
def test_gateway_chat_worker_reads_reasoning_content_deltas_from_chat_completions(tmp_path, monkeypatch):
session_dir = tmp_path / "sessions"
session_dir.mkdir()
monkeypatch.setattr(models, "SESSION_DIR", session_dir)
monkeypatch.setattr(models, "SESSION_INDEX_FILE", session_dir / "_index.json")
monkeypatch.setattr(models, "SESSIONS", OrderedDict())
class FakeResponse:
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
def __iter__(self):
yield b'data: {"choices":[{"delta":{"reasoning_content":"Let me ","content":"hel"}}]}\n\n'
yield b'data: {"choices":[{"delta":{"reasoning_content":"think","content":"lo"}}]}\n\n'
yield b'data: [DONE]\n\n'
monkeypatch.setenv("HERMES_WEBUI_GATEWAY_BASE_URL", "http://gateway.local")
monkeypatch.setattr(gateway_chat.urllib.request, "urlopen", lambda req, timeout=0: FakeResponse())
s = new_session()
stream_id = "stream-gateway-reasoning-content-test"
s.active_stream_id = stream_id
s.pending_user_message = "Say hello"
s.pending_attachments = []
s.pending_started_at = 123
s.save()
channel = create_stream_channel()
subscriber = channel.subscribe()
STREAMS[stream_id] = channel
gateway_chat._run_gateway_chat_streaming(
s.session_id,
"Say hello",
"test-model",
str(tmp_path),
stream_id,
[],
)
saved = models.get_session(s.session_id)
assert saved.messages[-1]["content"] == "hello"
assert saved.messages[-1]["reasoning"] == "Let me think"
reasoning_events = []
while not subscriber.empty():
item = subscriber.get_nowait()
if item[0] == "reasoning":
reasoning_events.append(item[1]["text"])
assert reasoning_events == ["Let me ", "think"]
def test_gateway_chat_worker_normalizes_prefill_slice_before_system_prefix(tmp_path, monkeypatch):
session_dir = tmp_path / "sessions"
session_dir.mkdir()