Add DuckLM service scripts and utility model benchmark

This commit is contained in:
2026-05-22 07:27:59 +08:00
parent e6b82f0376
commit ff98224eb6
24 changed files with 2492 additions and 74 deletions
+55
View File
@@ -56,6 +56,61 @@ def test_stream_chat_endpoint_emits_sse_reasoning_and_content(tmp_path, monkeypa
assert "answer" in body
def test_stream_chat_forwards_reasoning_toggle_to_thinker(tmp_path, monkeypatch):
monkeypatch.setenv("DUCK_DB_PATH", str(tmp_path / "duck.sqlite3"))
captured = {}
async def fake_chat(self, role, messages, temperature=None, max_output_tokens=None, response_format=None):
return ModelResponse(
role=role,
model="local-main",
content=json.dumps(
{
"kind": "action_directive",
"intent": "answer directly",
"risk_level": "none",
"actions": [],
}
),
reasoning_content=None,
raw={},
latency_ms=1.0,
)
async def fake_stream_chat(
self,
role,
messages,
temperature=None,
max_output_tokens=None,
response_format=None,
reasoning=None,
):
captured["role"] = role
captured["reasoning"] = reasoning
yield {"type": "content_delta", "delta": "answer"}
monkeypatch.setattr("duck_core.model_client.ModelClient.chat", fake_chat)
monkeypatch.setattr("duck_core.model_client.ModelClient.stream_chat", fake_stream_chat)
client = TestClient(create_app())
with client.stream(
"POST",
"/v1/chat/stream",
json={
"message": "hello",
"workspace": "./workspace",
"debug": True,
"reasoning": "off",
},
) as response:
body = "".join(response.iter_text())
assert response.status_code == 200
assert "event: done" in body
assert captured == {"role": "thinker", "reasoning": "off"}
def test_stream_chat_runs_memory_policy_and_reflection_after_completion(tmp_path, monkeypatch):
monkeypatch.setenv("DUCK_DB_PATH", str(tmp_path / "duck.sqlite3"))
+246
View File
@@ -0,0 +1,246 @@
import os
import subprocess
import textwrap
import time
from pathlib import Path
def test_duck_script_manages_llama_and_api(tmp_path):
fake_bin = tmp_path / "llama-server"
fake_bin.write_text(
textwrap.dedent(
"""\
#!/usr/bin/env bash
echo "fake llama-server $*" >&2
trap 'exit 0' TERM INT
while true; do sleep 1; done
"""
)
)
fake_bin.chmod(0o755)
fake_api = tmp_path / "fake-api.sh"
fake_api.write_text(
textwrap.dedent(
"""\
#!/usr/bin/env bash
echo "fake duck api $*" >&2
trap 'exit 0' TERM INT
while true; do sleep 1; done
"""
)
)
fake_api.chmod(0o755)
pid_file = tmp_path / "llama.pid"
log_file = tmp_path / "llama.log"
api_pid_file = tmp_path / "duck-api.pid"
api_log_file = tmp_path / "duck-api.log"
model_path = tmp_path / "model.gguf"
model_path.write_text("fake")
env = {
**os.environ,
"DUCK_LLAMA_SERVER_BIN": str(fake_bin),
"DUCK_MAIN_MODEL_PATH": str(model_path),
"DUCK_LLAMA_PID_FILE": str(pid_file),
"DUCK_LLAMA_LOG_FILE": str(log_file),
"DUCK_MAIN_PORT": "18081",
"DUCK_API_PID_FILE": str(api_pid_file),
"DUCK_API_LOG_FILE": str(api_log_file),
"DUCK_API_COMMAND": str(fake_api),
"DUCK_API_PORT": "18000",
}
script = "scripts/duck.sh"
stopped = subprocess.run([script, "status"], env=env, text=True, capture_output=True)
assert stopped.returncode == 3
assert "DuckLM API not running" in stopped.stdout
assert "llama-server not running" in stopped.stdout
started = subprocess.run([script, "start"], env=env, text=True, capture_output=True)
assert started.returncode == 0
assert "Starting llama-server" in started.stdout
assert "Starting DuckLM API" in started.stdout
assert "Status:" in started.stdout
assert "DuckLM API running" in started.stdout
assert "llama-server running" in started.stdout
assert api_pid_file.exists()
assert pid_file.exists()
try:
running = subprocess.run([script, "status"], env=env, text=True, capture_output=True)
assert running.returncode == 0
assert "DuckLM API running" in running.stdout
assert "llama-server running" in running.stdout
time.sleep(0.2)
logs = subprocess.run(
[script, "logs", "--lines", "20"], env=env, text=True, capture_output=True
)
assert logs.returncode == 0
assert "DuckLM API log" in logs.stdout
assert "fake duck api" in logs.stdout
assert "llama-server log" in logs.stdout
assert "--alias local-main" in logs.stdout
finally:
stopped = subprocess.run([script, "stop"], env=env, text=True, capture_output=True)
assert stopped.returncode == 0
assert not api_pid_file.exists()
assert not pid_file.exists()
def test_duck_mtp_script_starts_mtp_llama_for_duck_api(tmp_path):
fake_bin = tmp_path / "llama-server"
fake_bin.write_text(
textwrap.dedent(
"""\
#!/usr/bin/env bash
if [[ "${1:-}" == "--help" ]]; then
echo "supports --spec-type draft-mtp"
exit 0
fi
echo "fake mtp llama-server $*" >&2
trap 'exit 0' TERM INT
while true; do sleep 1; done
"""
)
)
fake_bin.chmod(0o755)
fake_api = tmp_path / "fake-api.sh"
fake_api.write_text(
textwrap.dedent(
"""\
#!/usr/bin/env bash
echo "fake duck api $*" >&2
trap 'exit 0' TERM INT
while true; do sleep 1; done
"""
)
)
fake_api.chmod(0o755)
main_model_path = tmp_path / "main.gguf"
mtp_model_path = tmp_path / "mtp.gguf"
main_model_path.write_text("main")
mtp_model_path.write_text("mtp")
pid_file = tmp_path / "llama-mtp.pid"
log_file = tmp_path / "llama-mtp.log"
api_pid_file = tmp_path / "duck-api-mtp.pid"
api_log_file = tmp_path / "duck-api-mtp.log"
env = {
**os.environ,
"DUCK_LLAMA_SERVER_BIN": str(fake_bin),
"DUCK_MAIN_MODEL_PATH": str(main_model_path),
"DUCK_MTP_MODEL_PATH": str(mtp_model_path),
"DUCK_LLAMA_PID_FILE": str(pid_file),
"DUCK_LLAMA_LOG_FILE": str(log_file),
"DUCK_MAIN_PORT": "18081",
"DUCK_API_PID_FILE": str(api_pid_file),
"DUCK_API_LOG_FILE": str(api_log_file),
"DUCK_API_COMMAND": str(fake_api),
"DUCK_API_PORT": "18000",
}
script = "scripts/duck-mtp.sh"
started = subprocess.run([script, "start"], env=env, text=True, capture_output=True)
assert started.returncode == 0
assert "Starting MTP llama-server" in started.stdout
assert "DuckLM API running" in started.stdout
assert "llama-server running" in started.stdout
try:
logs = subprocess.run(
[script, "logs", "--lines", "30"], env=env, text=True, capture_output=True
)
assert logs.returncode == 0
assert "--alias local-main" in logs.stdout
assert "--spec-type draft-mtp" in logs.stdout
assert f"-m {mtp_model_path}" in logs.stdout
assert "--model-draft" not in logs.stdout
assert str(main_model_path) not in logs.stdout
finally:
stopped = subprocess.run([script, "stop"], env=env, text=True, capture_output=True)
assert stopped.returncode == 0
assert not api_pid_file.exists()
assert not pid_file.exists()
def test_duck_mtp_script_sets_llama_bin_dir_library_path_for_help_check(tmp_path):
bin_dir = tmp_path / "build" / "bin"
bin_dir.mkdir(parents=True)
fake_bin = bin_dir / "llama-server"
fake_bin.write_text(
textwrap.dedent(
"""\
#!/usr/bin/env bash
case ":${LD_LIBRARY_PATH:-}:" in
*":$(dirname "$0"):"*) ;;
*)
echo "error while loading shared libraries: libllama-common.so.0" >&2
exit 127
;;
esac
if [[ "${1:-}" == "--help" ]]; then
echo "supports --spec-type draft-mtp"
exit 0
fi
echo "fake mtp llama-server $*" >&2
trap 'exit 0' TERM INT
while true; do sleep 1; done
"""
)
)
fake_bin.chmod(0o755)
fake_api = tmp_path / "fake-api.sh"
fake_api.write_text(
textwrap.dedent(
"""\
#!/usr/bin/env bash
trap 'exit 0' TERM INT
while true; do sleep 1; done
"""
)
)
fake_api.chmod(0o755)
main_model_path = tmp_path / "main.gguf"
mtp_model_path = tmp_path / "mtp.gguf"
main_model_path.write_text("main")
mtp_model_path.write_text("mtp")
env = {
**os.environ,
"LD_LIBRARY_PATH": "",
"DUCK_LLAMA_SERVER_BIN": str(fake_bin),
"DUCK_MAIN_MODEL_PATH": str(main_model_path),
"DUCK_MTP_MODEL_PATH": str(mtp_model_path),
"DUCK_LLAMA_PID_FILE": str(tmp_path / "llama-mtp.pid"),
"DUCK_LLAMA_LOG_FILE": str(tmp_path / "llama-mtp.log"),
"DUCK_MAIN_PORT": "18081",
"DUCK_API_PID_FILE": str(tmp_path / "duck-api-mtp.pid"),
"DUCK_API_LOG_FILE": str(tmp_path / "duck-api-mtp.log"),
"DUCK_API_COMMAND": str(fake_api),
"DUCK_API_PORT": "18000",
}
started = subprocess.run(
["scripts/duck-mtp.sh", "start"], env=env, text=True, capture_output=True
)
assert started.returncode == 0
try:
logs = subprocess.run(
["scripts/duck-mtp.sh", "logs", "--lines", "30"],
env=env,
text=True,
capture_output=True,
)
assert logs.returncode == 0
assert "--spec-type draft-mtp" in logs.stdout
assert "--model-draft" not in logs.stdout
finally:
subprocess.run(["scripts/duck-mtp.sh", "stop"], env=env, text=True, capture_output=True)
+51
View File
@@ -55,3 +55,54 @@ def test_start_main_script_manages_pid_status_stop_and_logs(tmp_path):
stopped = subprocess.run([script, "stop"], env=env, text=True, capture_output=True)
assert stopped.returncode == 0
assert not pid_file.exists()
def test_start_main_script_sets_llama_bin_dir_library_path(tmp_path):
bin_dir = tmp_path / "build" / "bin"
bin_dir.mkdir(parents=True)
fake_bin = bin_dir / "llama-server"
fake_bin.write_text(
textwrap.dedent(
"""\
#!/usr/bin/env bash
case ":${LD_LIBRARY_PATH:-}:" in
*":$(dirname "$0"):"*) ;;
*)
echo "error while loading shared libraries: libllama-common.so.0" >&2
exit 127
;;
esac
echo "fake llama-server $*" >&2
trap 'exit 0' TERM INT
while true; do sleep 1; done
"""
)
)
fake_bin.chmod(0o755)
model_path = tmp_path / "model.gguf"
model_path.write_text("fake")
pid_file = tmp_path / "llama.pid"
log_file = tmp_path / "llama.log"
env = {
**os.environ,
"LD_LIBRARY_PATH": "",
"DUCK_LLAMA_SERVER_BIN": str(fake_bin),
"DUCK_MAIN_MODEL_PATH": str(model_path),
"DUCK_LLAMA_PID_FILE": str(pid_file),
"DUCK_LLAMA_LOG_FILE": str(log_file),
"DUCK_MAIN_PORT": "18081",
}
script = "scripts/llama/start_main.sh"
started = subprocess.run([script, "start"], env=env, text=True, capture_output=True)
assert started.returncode == 0
try:
logs = subprocess.run(
[script, "logs", "--lines", "20"], env=env, text=True, capture_output=True
)
assert logs.returncode == 0
assert "--alias local-main" in logs.stdout
assert "error while loading shared libraries" not in logs.stdout
finally:
subprocess.run([script, "stop"], env=env, text=True, capture_output=True)
+26
View File
@@ -57,6 +57,32 @@ async def test_model_client_preserves_reasoning_content(monkeypatch):
assert response.reasoning_content == "private reasoning"
@pytest.mark.asyncio
async def test_model_client_adds_request_reasoning_options(monkeypatch):
payloads = []
async def fake_post(self, url, json):
payloads.append(json)
return httpx.Response(
200,
json={"choices": [{"message": {"role": "assistant", "content": "ok"}}]},
request=httpx.Request("POST", url),
)
monkeypatch.setattr(httpx.AsyncClient, "post", fake_post)
client = ModelClient("config/models.yaml")
await client.chat("thinker", [{"role": "user", "content": "hello"}], reasoning="on")
await client.chat("thinker", [{"role": "user", "content": "hello"}], reasoning="off")
assert payloads[0]["reasoning_format"] == "deepseek"
assert payloads[0]["chat_template_kwargs"] == {"enable_thinking": True}
assert "thinking_budget_tokens" not in payloads[0]
assert payloads[1]["reasoning_format"] == "deepseek"
assert payloads[1]["chat_template_kwargs"] == {"enable_thinking": False}
assert payloads[1]["thinking_budget_tokens"] == 0
@pytest.mark.asyncio
async def test_model_client_stream_chat_yields_reasoning_then_content(monkeypatch):
class FakeStreamResponse: