mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-20 15:11:09 +00:00
fix(provider): preserve streamed Codex output when response.completed.output is empty (#1928)
* fix: preserve streamed Codex output items * fix: prefer completed Codex output over streamed placeholders
This commit is contained in:
@@ -5,6 +5,7 @@ import json
|
||||
import pytest
|
||||
from langchain_core.messages import HumanMessage, SystemMessage
|
||||
|
||||
from deerflow.models import openai_codex_provider as codex_provider_module
|
||||
from deerflow.models.claude_provider import ClaudeChatModel
|
||||
from deerflow.models.credential_loader import CodexCliCredential
|
||||
from deerflow.models.openai_codex_provider import CodexChatModel
|
||||
@@ -147,3 +148,124 @@ def test_codex_provider_parses_valid_tool_arguments(monkeypatch):
|
||||
)
|
||||
|
||||
assert result.generations[0].message.tool_calls == [{"name": "bash", "args": {"cmd": "pwd"}, "id": "tc-1", "type": "tool_call"}]
|
||||
|
||||
|
||||
class _FakeResponseStream:
|
||||
def __init__(self, lines: list[str]):
|
||||
self._lines = lines
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
return False
|
||||
|
||||
def raise_for_status(self):
|
||||
return None
|
||||
|
||||
def iter_lines(self):
|
||||
yield from self._lines
|
||||
|
||||
|
||||
class _FakeHttpxClient:
|
||||
def __init__(self, lines: list[str], *_args, **_kwargs):
|
||||
self._lines = lines
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
return False
|
||||
|
||||
def stream(self, *_args, **_kwargs):
|
||||
return _FakeResponseStream(self._lines)
|
||||
|
||||
|
||||
def test_codex_provider_merges_streamed_output_items_when_completed_output_is_empty(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
CodexChatModel,
|
||||
"_load_codex_auth",
|
||||
lambda self: CodexCliCredential(access_token="token", account_id="acct"),
|
||||
)
|
||||
|
||||
lines = [
|
||||
'data: {"type":"response.output_item.done","output_index":0,"item":{"type":"message","content":[{"type":"output_text","text":"Hello from stream"}]}}',
|
||||
'data: {"type":"response.completed","response":{"model":"gpt-5.4","output":[],"usage":{"input_tokens":1,"output_tokens":2,"total_tokens":3}}}',
|
||||
]
|
||||
|
||||
monkeypatch.setattr(
|
||||
codex_provider_module.httpx,
|
||||
"Client",
|
||||
lambda *args, **kwargs: _FakeHttpxClient(lines, *args, **kwargs),
|
||||
)
|
||||
|
||||
model = CodexChatModel()
|
||||
response = model._stream_response(headers={}, payload={})
|
||||
parsed = model._parse_response(response)
|
||||
|
||||
assert response["output"] == [
|
||||
{
|
||||
"type": "message",
|
||||
"content": [{"type": "output_text", "text": "Hello from stream"}],
|
||||
}
|
||||
]
|
||||
assert parsed.generations[0].message.content == "Hello from stream"
|
||||
|
||||
|
||||
def test_codex_provider_orders_streamed_output_items_by_output_index(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
CodexChatModel,
|
||||
"_load_codex_auth",
|
||||
lambda self: CodexCliCredential(access_token="token", account_id="acct"),
|
||||
)
|
||||
|
||||
lines = [
|
||||
'data: {"type":"response.output_item.done","output_index":1,"item":{"type":"message","content":[{"type":"output_text","text":"Second"}]}}',
|
||||
'data: {"type":"response.output_item.done","output_index":0,"item":{"type":"message","content":[{"type":"output_text","text":"First"}]}}',
|
||||
'data: {"type":"response.completed","response":{"model":"gpt-5.4","output":[],"usage":{}}}',
|
||||
]
|
||||
|
||||
monkeypatch.setattr(
|
||||
codex_provider_module.httpx,
|
||||
"Client",
|
||||
lambda *args, **kwargs: _FakeHttpxClient(lines, *args, **kwargs),
|
||||
)
|
||||
|
||||
model = CodexChatModel()
|
||||
response = model._stream_response(headers={}, payload={})
|
||||
|
||||
assert [item["content"][0]["text"] for item in response["output"]] == [
|
||||
"First",
|
||||
"Second",
|
||||
]
|
||||
|
||||
|
||||
def test_codex_provider_preserves_completed_output_when_stream_only_has_placeholder(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
CodexChatModel,
|
||||
"_load_codex_auth",
|
||||
lambda self: CodexCliCredential(access_token="token", account_id="acct"),
|
||||
)
|
||||
|
||||
lines = [
|
||||
'data: {"type":"response.output_item.added","output_index":0,"item":{"type":"message","status":"in_progress","content":[]}}',
|
||||
'data: {"type":"response.completed","response":{"model":"gpt-5.4","output":[{"type":"message","content":[{"type":"output_text","text":"Final from completed"}]}],"usage":{}}}',
|
||||
]
|
||||
|
||||
monkeypatch.setattr(
|
||||
codex_provider_module.httpx,
|
||||
"Client",
|
||||
lambda *args, **kwargs: _FakeHttpxClient(lines, *args, **kwargs),
|
||||
)
|
||||
|
||||
model = CodexChatModel()
|
||||
response = model._stream_response(headers={}, payload={})
|
||||
parsed = model._parse_response(response)
|
||||
|
||||
assert response["output"] == [
|
||||
{
|
||||
"type": "message",
|
||||
"content": [{"type": "output_text", "text": "Final from completed"}],
|
||||
}
|
||||
]
|
||||
assert parsed.generations[0].message.content == "Final from completed"
|
||||
|
||||
Reference in New Issue
Block a user