feat(webui): stream live file edit events

This commit is contained in:
Xubin Ren
2026-05-18 22:01:33 +08:00
parent d4ade8f680
commit 7e2dbdef7d
19 changed files with 1873 additions and 48 deletions
+216
View File
@@ -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)