Add DuckLM service scripts and utility model benchmark
This commit is contained in:
@@ -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"))
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user