feat(webui): stream live file edit events
This commit is contained in:
@@ -164,6 +164,130 @@ def _fake_chat_stream_reasoning_chunks():
|
||||
return _stream()
|
||||
|
||||
|
||||
def _fake_chat_stream_tool_call_chunks():
|
||||
"""Mimic OpenAI-compatible streaming tool-call argument deltas."""
|
||||
|
||||
async def _stream():
|
||||
yield SimpleNamespace(
|
||||
choices=[
|
||||
SimpleNamespace(
|
||||
finish_reason=None,
|
||||
delta=SimpleNamespace(
|
||||
content=None,
|
||||
reasoning_content=None,
|
||||
reasoning=None,
|
||||
tool_calls=[
|
||||
SimpleNamespace(
|
||||
index=0,
|
||||
id="call_write",
|
||||
function=SimpleNamespace(
|
||||
name="write_file",
|
||||
arguments='{"path":"notes.md","content":"',
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
usage=None,
|
||||
)
|
||||
yield SimpleNamespace(
|
||||
choices=[
|
||||
SimpleNamespace(
|
||||
finish_reason=None,
|
||||
delta=SimpleNamespace(
|
||||
content=None,
|
||||
reasoning_content=None,
|
||||
reasoning=None,
|
||||
tool_calls=[
|
||||
SimpleNamespace(
|
||||
index=0,
|
||||
id=None,
|
||||
function=SimpleNamespace(name=None, arguments='line\\n"}'),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
usage=None,
|
||||
)
|
||||
yield SimpleNamespace(
|
||||
choices=[
|
||||
SimpleNamespace(
|
||||
finish_reason="tool_calls",
|
||||
delta=SimpleNamespace(
|
||||
content=None,
|
||||
reasoning_content=None,
|
||||
reasoning=None,
|
||||
tool_calls=None,
|
||||
),
|
||||
),
|
||||
],
|
||||
usage=SimpleNamespace(prompt_tokens=10, completion_tokens=5, total_tokens=15),
|
||||
)
|
||||
|
||||
return _stream()
|
||||
|
||||
|
||||
def _fake_chat_stream_legacy_function_call_chunks():
|
||||
"""Mimic older OpenAI-compatible ``delta.function_call`` chunks."""
|
||||
|
||||
async def _stream():
|
||||
yield SimpleNamespace(
|
||||
choices=[
|
||||
SimpleNamespace(
|
||||
finish_reason=None,
|
||||
delta=SimpleNamespace(
|
||||
content=None,
|
||||
reasoning_content=None,
|
||||
reasoning=None,
|
||||
tool_calls=None,
|
||||
function_call=SimpleNamespace(
|
||||
name="write_file",
|
||||
arguments='{"path":"notes.md","content":"',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
usage=None,
|
||||
)
|
||||
yield SimpleNamespace(
|
||||
choices=[
|
||||
SimpleNamespace(
|
||||
finish_reason=None,
|
||||
delta=SimpleNamespace(
|
||||
content=None,
|
||||
reasoning_content=None,
|
||||
reasoning=None,
|
||||
tool_calls=None,
|
||||
function_call=SimpleNamespace(
|
||||
name=None,
|
||||
arguments='line\\n"}',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
usage=None,
|
||||
)
|
||||
yield SimpleNamespace(
|
||||
choices=[
|
||||
SimpleNamespace(
|
||||
finish_reason="function_call",
|
||||
delta=SimpleNamespace(
|
||||
content=None,
|
||||
reasoning_content=None,
|
||||
reasoning=None,
|
||||
tool_calls=None,
|
||||
function_call=None,
|
||||
),
|
||||
),
|
||||
],
|
||||
usage=SimpleNamespace(prompt_tokens=10, completion_tokens=5, total_tokens=15),
|
||||
)
|
||||
|
||||
return _stream()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_openai_compat_stream_forwards_reasoning_deltas_deepseek_style() -> None:
|
||||
"""Regression: DeepSeek-V4 / reasoner expose ``delta.reasoning_content`` during streaming."""
|
||||
@@ -202,6 +326,98 @@ async def test_openai_compat_stream_forwards_reasoning_deltas_deepseek_style() -
|
||||
mock_chat.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("provider_name", "model"),
|
||||
[
|
||||
("openai", "gpt-4o"),
|
||||
("deepseek", "deepseek-chat"),
|
||||
("minimax", "MiniMax-M2.7"),
|
||||
("zhipu", "glm-4.6"),
|
||||
],
|
||||
)
|
||||
async def test_openai_compat_stream_forwards_tool_call_argument_deltas(
|
||||
provider_name: str,
|
||||
model: str,
|
||||
) -> None:
|
||||
mock_chat = AsyncMock(return_value=_fake_chat_stream_tool_call_chunks())
|
||||
spec = find_by_name(provider_name)
|
||||
deltas: list[dict] = []
|
||||
|
||||
async def on_tool_delta(delta: dict) -> None:
|
||||
deltas.append(delta)
|
||||
|
||||
with patch("nanobot.providers.openai_compat_provider.AsyncOpenAI") as mock_openai:
|
||||
client_instance = mock_openai.return_value
|
||||
client_instance.chat.completions.create = mock_chat
|
||||
|
||||
provider = OpenAICompatProvider(
|
||||
api_key="sk-test",
|
||||
default_model=model,
|
||||
spec=spec,
|
||||
)
|
||||
result = await provider.chat_stream(
|
||||
messages=[{"role": "user", "content": "write"}],
|
||||
tools=[{"type": "function", "function": {"name": "write_file"}}],
|
||||
model=model,
|
||||
on_tool_call_delta=on_tool_delta,
|
||||
)
|
||||
|
||||
assert deltas == [
|
||||
{
|
||||
"index": 0,
|
||||
"call_id": "call_write",
|
||||
"name": "write_file",
|
||||
"arguments_delta": '{"path":"notes.md","content":"',
|
||||
},
|
||||
{"index": 0, "call_id": "", "name": "", "arguments_delta": 'line\\n"}'},
|
||||
]
|
||||
assert result.tool_calls[0].name == "write_file"
|
||||
assert result.tool_calls[0].arguments == {"path": "notes.md", "content": "line\n"}
|
||||
kwargs = mock_chat.await_args.kwargs
|
||||
if provider_name == "zhipu":
|
||||
assert kwargs["extra_body"]["tool_stream"] is True
|
||||
else:
|
||||
assert kwargs.get("extra_body", {}).get("tool_stream") is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_openai_compat_stream_forwards_legacy_function_call_argument_deltas() -> None:
|
||||
mock_chat = AsyncMock(return_value=_fake_chat_stream_legacy_function_call_chunks())
|
||||
deltas: list[dict] = []
|
||||
|
||||
async def on_tool_delta(delta: dict) -> None:
|
||||
deltas.append(delta)
|
||||
|
||||
with patch("nanobot.providers.openai_compat_provider.AsyncOpenAI") as mock_openai:
|
||||
client_instance = mock_openai.return_value
|
||||
client_instance.chat.completions.create = mock_chat
|
||||
|
||||
provider = OpenAICompatProvider(
|
||||
api_key="sk-test",
|
||||
default_model="deepseek-chat",
|
||||
spec=find_by_name("deepseek"),
|
||||
)
|
||||
result = await provider.chat_stream(
|
||||
messages=[{"role": "user", "content": "write"}],
|
||||
tools=[{"type": "function", "function": {"name": "write_file"}}],
|
||||
model="deepseek-chat",
|
||||
on_tool_call_delta=on_tool_delta,
|
||||
)
|
||||
|
||||
assert deltas == [
|
||||
{
|
||||
"index": 0,
|
||||
"call_id": "",
|
||||
"name": "write_file",
|
||||
"arguments_delta": '{"path":"notes.md","content":"',
|
||||
},
|
||||
{"index": 0, "call_id": "", "name": "", "arguments_delta": 'line\\n"}'},
|
||||
]
|
||||
assert result.tool_calls[0].name == "write_file"
|
||||
assert result.tool_calls[0].arguments == {"path": "notes.md", "content": "line\n"}
|
||||
|
||||
|
||||
class _FakeResponsesError(Exception):
|
||||
def __init__(self, status_code: int, text: str):
|
||||
super().__init__(text)
|
||||
|
||||
Reference in New Issue
Block a user