Skip to content

Commit 368f9ed

Browse files
committed
fix: emit final response after stream parse fallback
1 parent 284c408 commit 368f9ed

2 files changed

Lines changed: 75 additions & 2 deletions

File tree

astrbot/core/provider/sources/openai_source.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,9 @@ async def _query_stream(
655655
llm_response = LLMResponse("assistant", is_chunk=True)
656656

657657
state = ChatCompletionStreamState()
658+
streamed_text_parts: list[str] = []
659+
streamed_reasoning_parts: list[str] = []
660+
latest_usage = None
658661

659662
async for chunk in stream:
660663
choice = chunk.choices[0] if chunk.choices else None
@@ -688,20 +691,24 @@ async def _query_stream(
688691
llm_response.completion_text = ""
689692
if reasoning is not None:
690693
llm_response.reasoning_content = reasoning
694+
streamed_reasoning_parts.append(reasoning)
691695
_y = True
692696
if delta and delta.content:
693697
# Don't strip streaming chunks to preserve spaces between words
694698
completion_text = self._normalize_content(delta.content, strip=False)
699+
streamed_text_parts.append(completion_text)
695700
llm_response.result_chain = MessageChain(
696701
chain=[Comp.Plain(completion_text)],
697702
)
698703
_y = True
699704
if chunk.usage:
700705
llm_response.usage = self._extract_usage(chunk.usage)
706+
latest_usage = llm_response.usage
701707
elif choice and (choice_usage := getattr(choice, "usage", None)):
702708
# Workaround for some providers that only return usage in choices[].usage, e.g. MoonshotAI
703709
# See https://github.com/AstrBotDevs/AstrBot/issues/6614
704710
llm_response.usage = self._extract_usage(choice_usage)
711+
latest_usage = llm_response.usage
705712
state.current_completion_snapshot.usage = choice_usage
706713
if _y:
707714
yield llm_response
@@ -712,8 +719,15 @@ async def _query_stream(
712719
yield llm_response
713720
except Exception as e:
714721
logger.error("get_final_completion error: " + str(e))
715-
# 流式内容已通过 yield 发出,记录错误后正常结束即可
716-
return
722+
if streamed_text_parts or streamed_reasoning_parts:
723+
yield LLMResponse(
724+
"assistant",
725+
completion_text="".join(streamed_text_parts),
726+
reasoning_content="".join(streamed_reasoning_parts) or None,
727+
usage=latest_usage,
728+
)
729+
return
730+
raise
717731

718732
def _extract_reasoning_content(
719733
self,

tests/test_openai_source.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1325,6 +1325,65 @@ async def fake_create(**kwargs):
13251325
await provider.terminate()
13261326

13271327

1328+
@pytest.mark.asyncio
1329+
async def test_query_stream_yields_final_response_when_final_completion_parse_fails(
1330+
monkeypatch,
1331+
):
1332+
provider = _make_provider()
1333+
try:
1334+
chunks = [
1335+
ChatCompletionChunk.model_validate(
1336+
{
1337+
"id": "chatcmpl-stream",
1338+
"object": "chat.completion.chunk",
1339+
"created": 0,
1340+
"model": "gpt-4o-mini",
1341+
"choices": [
1342+
{
1343+
"index": 0,
1344+
"delta": {
1345+
"role": "assistant",
1346+
"content": "hello",
1347+
},
1348+
"finish_reason": None,
1349+
}
1350+
],
1351+
}
1352+
)
1353+
]
1354+
1355+
async def fake_stream():
1356+
for chunk in chunks:
1357+
yield chunk
1358+
1359+
async def fake_create(**kwargs):
1360+
return fake_stream()
1361+
1362+
async def fake_parse_completion(completion, tools):
1363+
raise EmptyModelOutputError("final completion was empty")
1364+
1365+
monkeypatch.setattr(provider.client.chat.completions, "create", fake_create)
1366+
monkeypatch.setattr(provider, "_parse_openai_completion", fake_parse_completion)
1367+
1368+
responses = [
1369+
response
1370+
async for response in provider._query_stream(
1371+
payloads={
1372+
"model": "gpt-4o-mini",
1373+
"messages": [{"role": "user", "content": "hello"}],
1374+
},
1375+
tools=None,
1376+
)
1377+
]
1378+
1379+
assert len(responses) == 2
1380+
assert responses[0].is_chunk
1381+
assert not responses[-1].is_chunk
1382+
assert responses[-1].completion_text == "hello"
1383+
finally:
1384+
await provider.terminate()
1385+
1386+
13281387
@pytest.mark.asyncio
13291388
async def test_query_filters_empty_assistant_message_without_tool_calls(monkeypatch):
13301389
"""Test that empty assistant messages without tool_calls are filtered out."""

0 commit comments

Comments
 (0)