Skip to content

Commit 5f0d6b2

Browse files
committed
Return reasoning content from model output
Protocol conversion should use the returned reasoning payload as the source of truth. MODEL_PARAMETER_RULES can still control model-side thinking, but OpenAI and AG-UI responses should not hide non-empty reasoning_content when the env flag says thinking is false. Constraint: User requested removal of protocol-level thinking_enabled = is_thinking_enabled_from_env() gating. Rejected: Keep env-based response suppression | runtime parameters can drift from the model output and hide returned reasoning. Confidence: high Scope-risk: narrow Directive: Do not reintroduce protocol-level reasoning env gates; only emit reasoning when returned reasoning_content is non-empty. Tested: uv run --python 3.11 --dev --extra server pytest -q tests/unittests/server/test_openai_protocol.py tests/unittests/server/test_agui_protocol.py tests/unittests/server/test_reasoning.py Tested: uv run --python 3.11 --dev --extra server pytest -q tests/unittests/server Tested: uv run --python 3.11 --dev --extra server ruff check agentrun/server/openai_protocol.py agentrun/server/agui_protocol.py tests/unittests/server/test_openai_protocol.py tests/unittests/server/test_agui_protocol.py Tested: git diff --check Change-Id: I638efa7ca19bf8ed9417fb1922d43205d4d52b65 Not-tested: Remote GitHub CI result pending after push.
1 parent 3755e4a commit 5f0d6b2

4 files changed

Lines changed: 98 additions & 82 deletions

File tree

agentrun/server/agui_protocol.py

Lines changed: 20 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,7 @@
3131
import pydash
3232

3333
from ..utils.helper import merge, MergeOptions
34-
from ..utils.reasoning import (
35-
get_reasoning_content,
36-
is_thinking_enabled_from_env,
37-
)
34+
from ..utils.reasoning import get_reasoning_content
3835
from .model import (
3936
AgentEvent,
4037
AgentRequest,
@@ -466,8 +463,6 @@ def _process_event_with_boundaries(
466463
ToolCallStartEvent,
467464
)
468465

469-
thinking_enabled = is_thinking_enabled_from_env()
470-
471466
# RAW 事件直接透传
472467
if event.event == EventType.RAW:
473468
raw_data = event.data.get("raw", "")
@@ -478,34 +473,31 @@ def _process_event_with_boundaries(
478473
return
479474

480475
if event.event == EventType.REASONING:
481-
if thinking_enabled:
482-
reasoning_content = (
483-
event.data.get("delta")
484-
or get_reasoning_content(event.data)
485-
or ""
486-
)
487-
if reasoning_content:
488-
for sse_data in state.end_text_if_open(self._encoder):
489-
yield sse_data
490-
for sse_data in state.end_all_tools(self._encoder):
491-
yield sse_data
492-
for sse_data in state.ensure_reasoning_started():
493-
yield sse_data
494-
yield _encode_reasoning_event(
495-
"REASONING_MESSAGE_CONTENT",
496-
messageId=state.reasoning.message_id,
497-
delta=reasoning_content,
498-
)
476+
reasoning_content = (
477+
event.data.get("delta")
478+
or get_reasoning_content(event.data)
479+
or ""
480+
)
481+
if reasoning_content:
482+
for sse_data in state.end_text_if_open(self._encoder):
483+
yield sse_data
484+
for sse_data in state.end_all_tools(self._encoder):
485+
yield sse_data
486+
for sse_data in state.ensure_reasoning_started():
487+
yield sse_data
488+
yield _encode_reasoning_event(
489+
"REASONING_MESSAGE_CONTENT",
490+
messageId=state.reasoning.message_id,
491+
delta=reasoning_content,
492+
)
499493
return
500494

501495
# TEXT 事件:在首个 TEXT 前注入 TEXT_MESSAGE_START
502496
# AG-UI 协议要求:发送 TEXT_MESSAGE_START 前必须先结束所有未结束的 TOOL_CALL
503497
if event.event == EventType.TEXT:
504-
addition = self._strip_reasoning_from_addition(
505-
event.addition, thinking_enabled
506-
)
498+
addition = self._strip_reasoning_from_addition(event.addition)
507499
addition_reasoning = get_reasoning_content(event.addition or {})
508-
if thinking_enabled and addition_reasoning:
500+
if addition_reasoning:
509501
for sse_data in state.ensure_reasoning_started():
510502
yield sse_data
511503
yield _encode_reasoning_event(
@@ -874,7 +866,6 @@ def _apply_addition(
874866
def _strip_reasoning_from_addition(
875867
self,
876868
addition: Optional[Dict[str, Any]],
877-
thinking_enabled: bool,
878869
) -> Optional[Dict[str, Any]]:
879870
if not addition:
880871
return addition
@@ -890,8 +881,6 @@ def _strip_reasoning_from_addition(
890881
else:
891882
stripped.pop("additional_kwargs", None)
892883

893-
if not thinking_enabled:
894-
return stripped
895884
return stripped or None
896885

897886
async def _error_stream(self, message: str) -> AsyncIterator[str]:

agentrun/server/openai_protocol.py

Lines changed: 19 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,7 @@
1515
from fastapi.responses import JSONResponse, StreamingResponse
1616
import pydash
1717

18-
from ..utils.reasoning import (
19-
get_reasoning_content,
20-
is_thinking_enabled_from_env,
21-
)
18+
from ..utils.reasoning import get_reasoning_content
2219
from ..utils.helper import merge, MergeOptions
2320
from .model import (
2421
AgentEvent,
@@ -304,7 +301,6 @@ async def _format_stream(
304301
# 状态追踪
305302
sent_role = False
306303
has_text = False
307-
thinking_enabled = is_thinking_enabled_from_env()
308304
tool_call_index = -1 # 从 -1 开始,第一个工具调用时变为 0
309305
# 工具调用状态:{tool_id: {"started": bool, "index": int}}
310306
tool_call_states: Dict[str, Dict[str, Any]] = {}
@@ -341,19 +337,18 @@ async def _format_stream(
341337
event.addition_merge_options,
342338
)
343339

344-
self._apply_reasoning_gate(delta, thinking_enabled)
340+
self._promote_reasoning_content(delta)
345341
yield self._build_chunk(context, delta)
346342
continue
347343

348344
if event.event == EventType.REASONING:
349-
if thinking_enabled:
350-
reasoning_content = event.data.get("delta", "")
351-
if reasoning_content:
352-
has_text = True
353-
yield self._build_chunk(
354-
context,
355-
{"reasoning_content": reasoning_content},
356-
)
345+
reasoning_content = event.data.get("delta", "")
346+
if reasoning_content:
347+
has_text = True
348+
yield self._build_chunk(
349+
context,
350+
{"reasoning_content": reasoning_content},
351+
)
357352
continue
358353

359354
# TOOL_CALL_CHUNK 事件
@@ -401,7 +396,7 @@ async def _format_stream(
401396
event.addition_merge_options,
402397
)
403398

404-
self._apply_reasoning_gate(delta, thinking_enabled)
399+
self._promote_reasoning_content(delta)
405400
yield self._build_chunk(context, delta)
406401
continue
407402

@@ -477,7 +472,6 @@ def _format_non_stream(
477472
"""
478473
content_parts: List[str] = []
479474
reasoning_parts: List[str] = []
480-
thinking_enabled = is_thinking_enabled_from_env()
481475
# 工具调用状态:{tool_id: {id, name, arguments}}
482476
tool_call_map: Dict[str, Dict[str, Any]] = {}
483477
has_tool_calls = False
@@ -486,12 +480,12 @@ def _format_non_stream(
486480
if event.event == EventType.TEXT:
487481
content_parts.append(event.data.get("delta", ""))
488482
reasoning_content = get_reasoning_content(event.addition or {})
489-
if thinking_enabled and reasoning_content:
483+
if reasoning_content:
490484
reasoning_parts.append(reasoning_content)
491485

492486
elif event.event == EventType.REASONING:
493487
reasoning_content = event.data.get("delta", "")
494-
if thinking_enabled and reasoning_content:
488+
if reasoning_content:
495489
reasoning_parts.append(reasoning_content)
496490

497491
elif event.event == EventType.TOOL_CALL_CHUNK:
@@ -564,18 +558,14 @@ def _apply_addition(
564558

565559
return merge(delta, addition, **(merge_options or {}))
566560

567-
def _apply_reasoning_gate(
568-
self,
569-
payload: Dict[str, Any],
570-
thinking_enabled: bool,
571-
) -> None:
572-
if thinking_enabled:
573-
reasoning_content = get_reasoning_content(payload)
574-
if reasoning_content is not None:
575-
payload["reasoning_content"] = reasoning_content
576-
return
577-
561+
def _promote_reasoning_content(self, payload: Dict[str, Any]) -> None:
562+
reasoning_content = get_reasoning_content(payload)
578563
payload.pop("reasoning_content", None)
579564
additional_kwargs = payload.get("additional_kwargs")
580565
if isinstance(additional_kwargs, dict):
581566
additional_kwargs.pop("reasoning_content", None)
567+
if not additional_kwargs:
568+
payload.pop("additional_kwargs", None)
569+
570+
if reasoning_content:
571+
payload["reasoning_content"] = reasoning_content

tests/unittests/server/test_agui_protocol.py

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1196,7 +1196,7 @@ async def invoke_agent(request: AgentRequest):
11961196

11971197

11981198
class TestAGUIReasoningContent:
1199-
"""测试 AG-UI reasoning 事件输出开关"""
1199+
"""测试 AG-UI reasoning 事件输出"""
12001200

12011201
def get_client(self, invoke_agent):
12021202
server = AgentRunServer(invoke_agent=invoke_agent)
@@ -1228,7 +1228,7 @@ async def invoke_agent(request: AgentRequest):
12281228
assert reasoning_event["delta"] == "thinking"
12291229
assert "TEXT_MESSAGE_CONTENT" in types
12301230

1231-
def test_stream_suppresses_reasoning_when_thinking_disabled(
1231+
def test_stream_includes_reasoning_when_thinking_disabled(
12321232
self, monkeypatch
12331233
):
12341234
monkeypatch.setenv("MODEL_PARAMETER_RULES", '{"thinking": false}')
@@ -1246,9 +1246,14 @@ async def invoke_agent(request: AgentRequest):
12461246
)
12471247

12481248
events = _agui_sse_events(response)
1249-
assert "REASONING_MESSAGE_CONTENT" not in [
1250-
event["type"] for event in events
1251-
]
1249+
types = [event["type"] for event in events]
1250+
reasoning_event = next(
1251+
event
1252+
for event in events
1253+
if event["type"] == "REASONING_MESSAGE_CONTENT"
1254+
)
1255+
assert "REASONING_START" in types
1256+
assert reasoning_event["delta"] == "thinking"
12521257
text_event = next(
12531258
event for event in events if event["type"] == "TEXT_MESSAGE_CONTENT"
12541259
)
@@ -1257,7 +1262,7 @@ async def invoke_agent(request: AgentRequest):
12571262
def test_stream_promotes_chunk_additional_kwargs_reasoning(
12581263
self, monkeypatch
12591264
):
1260-
monkeypatch.setenv("MODEL_PARAMETER_RULES", '{"thinking": true}')
1265+
monkeypatch.setenv("MODEL_PARAMETER_RULES", '{"thinking": false}')
12611266

12621267
async def invoke_agent(request: AgentRequest):
12631268
yield SimpleNamespace(
@@ -1282,9 +1287,7 @@ async def invoke_agent(request: AgentRequest):
12821287
assert reasoning_event["delta"] == "thinking"
12831288
assert text_event["delta"] == "answer"
12841289

1285-
def test_text_addition_reasoning_is_emitted_before_text(
1286-
self, monkeypatch
1287-
):
1290+
def test_text_addition_reasoning_is_emitted_before_text(self, monkeypatch):
12881291
monkeypatch.setenv("MODEL_PARAMETER_RULES", '{"thinking": true}')
12891292

12901293
async def invoke_agent(request: AgentRequest):
@@ -1314,7 +1317,7 @@ async def invoke_agent(request: AgentRequest):
13141317
assert text_event["delta"] == "answer"
13151318
assert "additional_kwargs" not in text_event
13161319

1317-
def test_text_addition_reasoning_is_stripped_when_thinking_disabled(
1320+
def test_text_addition_reasoning_is_emitted_when_thinking_disabled(
13181321
self, monkeypatch
13191322
):
13201323
monkeypatch.setenv("MODEL_PARAMETER_RULES", '{"thinking": false}')
@@ -1335,7 +1338,11 @@ async def invoke_agent(request: AgentRequest):
13351338

13361339
events = _agui_sse_events(response)
13371340
types = [event["type"] for event in events]
1338-
assert all(not event_type.startswith("REASONING") for event_type in types)
1341+
assert types.index("REASONING_MESSAGE_CONTENT") < types.index(
1342+
"TEXT_MESSAGE_START"
1343+
)
1344+
assert "REASONING_MESSAGE_END" in types
1345+
assert "REASONING_END" in types
13391346
text_event = next(
13401347
event for event in events if event["type"] == "TEXT_MESSAGE_CONTENT"
13411348
)

tests/unittests/server/test_openai_protocol.py

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,7 +1017,7 @@ def invoke_agent(request: AgentRequest):
10171017

10181018

10191019
class TestOpenAIReasoningContent:
1020-
"""测试 OpenAI reasoning_content 输出开关"""
1020+
"""测试 OpenAI reasoning_content 输出"""
10211021

10221022
def get_client(self, invoke_agent):
10231023
server = AgentRunServer(invoke_agent=invoke_agent)
@@ -1042,10 +1042,12 @@ async def invoke_agent(request: AgentRequest):
10421042
)
10431043

10441044
events = _openai_sse_events(response)
1045-
assert events[0]["choices"][0]["delta"]["reasoning_content"] == "thinking"
1045+
assert (
1046+
events[0]["choices"][0]["delta"]["reasoning_content"] == "thinking"
1047+
)
10461048
assert events[1]["choices"][0]["delta"]["content"] == "answer"
10471049

1048-
def test_stream_suppresses_reasoning_when_thinking_disabled(
1050+
def test_stream_includes_reasoning_when_thinking_disabled(
10491051
self, monkeypatch
10501052
):
10511053
monkeypatch.setenv("MODEL_PARAMETER_RULES", '{"thinking": false}')
@@ -1066,11 +1068,10 @@ async def invoke_agent(request: AgentRequest):
10661068
)
10671069

10681070
events = _openai_sse_events(response)
1069-
assert all(
1070-
"reasoning_content" not in event["choices"][0]["delta"]
1071-
for event in events
1071+
assert (
1072+
events[0]["choices"][0]["delta"]["reasoning_content"] == "thinking"
10721073
)
1073-
assert events[0]["choices"][0]["delta"]["content"] == "answer"
1074+
assert events[1]["choices"][0]["delta"]["content"] == "answer"
10741075

10751076
def test_non_stream_includes_reasoning_when_thinking_enabled(
10761077
self, monkeypatch
@@ -1098,7 +1099,7 @@ def invoke_agent(request: AgentRequest):
10981099
assert message["content"] == "answer"
10991100
assert message["reasoning_content"] == "thinking"
11001101

1101-
def test_non_stream_suppresses_reasoning_when_thinking_disabled(
1102+
def test_non_stream_includes_reasoning_when_thinking_disabled(
11021103
self, monkeypatch
11031104
):
11041105
monkeypatch.setenv("MODEL_PARAMETER_RULES", '{"thinking": false}')
@@ -1122,12 +1123,12 @@ def invoke_agent(request: AgentRequest):
11221123

11231124
message = response.json()["choices"][0]["message"]
11241125
assert message["content"] == "answer"
1125-
assert "reasoning_content" not in message
1126+
assert message["reasoning_content"] == "thinking"
11261127

11271128
def test_stream_promotes_chunk_additional_kwargs_reasoning(
11281129
self, monkeypatch
11291130
):
1130-
monkeypatch.setenv("MODEL_PARAMETER_RULES", '{"thinking": true}')
1131+
monkeypatch.setenv("MODEL_PARAMETER_RULES", '{"thinking": false}')
11311132

11321133
async def invoke_agent(request: AgentRequest):
11331134
yield SimpleNamespace(
@@ -1144,9 +1145,38 @@ async def invoke_agent(request: AgentRequest):
11441145
)
11451146

11461147
events = _openai_sse_events(response)
1147-
assert events[0]["choices"][0]["delta"]["reasoning_content"] == "thinking"
1148+
assert (
1149+
events[0]["choices"][0]["delta"]["reasoning_content"] == "thinking"
1150+
)
11481151
assert events[1]["choices"][0]["delta"]["content"] == "answer"
11491152

1153+
def test_stream_promotes_text_addition_reasoning_when_thinking_disabled(
1154+
self, monkeypatch
1155+
):
1156+
monkeypatch.setenv("MODEL_PARAMETER_RULES", '{"thinking": false}')
1157+
1158+
async def invoke_agent(request: AgentRequest):
1159+
yield AgentEvent(
1160+
event=EventType.TEXT,
1161+
data={"delta": "answer"},
1162+
addition={
1163+
"additional_kwargs": {"reasoning_content": "thinking"}
1164+
},
1165+
)
1166+
1167+
response = self.get_client(invoke_agent).post(
1168+
"/openai/v1/chat/completions",
1169+
json={
1170+
"messages": [{"role": "user", "content": "Hi"}],
1171+
"stream": True,
1172+
},
1173+
)
1174+
1175+
delta = _openai_sse_events(response)[0]["choices"][0]["delta"]
1176+
assert delta["content"] == "answer"
1177+
assert delta["reasoning_content"] == "thinking"
1178+
assert "additional_kwargs" not in delta
1179+
11501180
def test_parses_request_message_reasoning_content(self):
11511181
captured_request = {}
11521182

0 commit comments

Comments
 (0)