Initial commit: llamacppctl – llama.cpp Docker server control CLI
Steuert einen llama.cpp-Server als Docker-Container: --start/--check/--stop/
--change/--chat, INI-Konfiguration (builtin defaults -> [default] ->
[model.<profile>] -> CLI), SSRF-gehärtete Prompt-Eingabe (Datei/HTTPS-URL),
File-Locking für --start/--change und ein OpenAI-kompatibler HTTP-Layer.
Enthält u. a.:
- Env-Var-Expansion in hf_home (hf_home = ${HF_HOME})
- konfigurierbares Chat-Antwortbudget (max_tokens/chat_temperature,
CLI: --max-tokens/--chat-temp); temperature defer an Server-Default
- DNS-Pinning gegen DNS-Rebinding bei URL-Quellen
- dry-run als nebenwirkungsfreie Vorschau (kein Lock/Removal/Modell-Check)
- 98 Tests (pytest)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
commit
3158d16f9b
32 changed files with 3912 additions and 0 deletions
132
tests/test_http_ops.py
Normal file
132
tests/test_http_ops.py
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
import requests # noqa: E402
|
||||
|
||||
from llamacppctl.http_ops import ( # noqa: E402
|
||||
HttpError,
|
||||
base_url,
|
||||
chat_completion,
|
||||
chat_completion_text,
|
||||
check_health,
|
||||
check_models,
|
||||
wait_until_ready,
|
||||
)
|
||||
from llamacppctl.schema import PromptConfig, ServerConfig # noqa: E402
|
||||
from tests.test_docker_ops import make_cfg # noqa: E402
|
||||
|
||||
|
||||
def test_base_url():
|
||||
cfg = make_cfg(host_port=8001)
|
||||
assert base_url(cfg) == "http://127.0.0.1:8001/v1"
|
||||
|
||||
|
||||
def test_check_health_ok():
|
||||
cfg = make_cfg()
|
||||
with patch("requests.get") as mock_get:
|
||||
mock_get.return_value = MagicMock(ok=True)
|
||||
assert check_health(cfg) is True
|
||||
|
||||
|
||||
def test_check_health_exception_returns_false():
|
||||
cfg = make_cfg()
|
||||
with patch("requests.get", side_effect=requests.RequestException("boom")):
|
||||
assert check_health(cfg) is False
|
||||
|
||||
|
||||
def test_check_models_ok():
|
||||
cfg = make_cfg()
|
||||
with patch("requests.get") as mock_get:
|
||||
mock_get.return_value = MagicMock(ok=True)
|
||||
assert check_models(cfg) is True
|
||||
|
||||
|
||||
def test_chat_completion_posts_expected_payload():
|
||||
cfg = make_cfg()
|
||||
prompt_cfg = PromptConfig(system_prompt="sys", user_prompt="hi", max_tokens=32, temperature=0.1)
|
||||
with patch("requests.post") as mock_post:
|
||||
mock_post.return_value = MagicMock(status_code=200)
|
||||
chat_completion(cfg, prompt_cfg)
|
||||
_, kwargs = mock_post.call_args
|
||||
payload = kwargs["json"]
|
||||
assert payload["messages"][0] == {"role": "system", "content": "sys"}
|
||||
assert payload["messages"][1] == {"role": "user", "content": "hi"}
|
||||
assert payload["max_tokens"] == 32
|
||||
|
||||
|
||||
def test_chat_completion_text_success():
|
||||
cfg = make_cfg()
|
||||
prompt_cfg = PromptConfig(user_prompt="hi")
|
||||
with patch("requests.post") as mock_post:
|
||||
mock_resp = MagicMock(status_code=200)
|
||||
mock_resp.json.return_value = {"choices": [{"message": {"content": "ok"}}]}
|
||||
mock_post.return_value = mock_resp
|
||||
assert chat_completion_text(cfg, prompt_cfg) == "ok"
|
||||
|
||||
|
||||
def test_chat_completion_text_honors_chat_endpoint():
|
||||
cfg = make_cfg(host_port=8001, chat_endpoint="/custom/chat")
|
||||
prompt_cfg = PromptConfig(user_prompt="hi")
|
||||
with patch("requests.post") as mock_post:
|
||||
mock_resp = MagicMock(status_code=200)
|
||||
mock_resp.json.return_value = {"choices": [{"message": {"content": "ok"}}]}
|
||||
mock_post.return_value = mock_resp
|
||||
chat_completion_text(cfg, prompt_cfg)
|
||||
args, _ = mock_post.call_args
|
||||
assert args[0] == "http://127.0.0.1:8001/custom/chat"
|
||||
|
||||
|
||||
def test_chat_completion_text_http_error():
|
||||
cfg = make_cfg()
|
||||
prompt_cfg = PromptConfig(user_prompt="hi")
|
||||
with patch("requests.post") as mock_post:
|
||||
mock_post.return_value = MagicMock(status_code=500, text="server error")
|
||||
try:
|
||||
chat_completion_text(cfg, prompt_cfg)
|
||||
assert False, "expected HttpError"
|
||||
except HttpError:
|
||||
pass
|
||||
|
||||
|
||||
def test_chat_completion_text_transport_error():
|
||||
cfg = make_cfg()
|
||||
prompt_cfg = PromptConfig(user_prompt="hi")
|
||||
with patch("requests.post", side_effect=requests.RequestException("down")):
|
||||
try:
|
||||
chat_completion_text(cfg, prompt_cfg)
|
||||
assert False, "expected HttpError"
|
||||
except HttpError:
|
||||
pass
|
||||
|
||||
|
||||
def test_chat_completion_text_malformed_response():
|
||||
cfg = make_cfg()
|
||||
prompt_cfg = PromptConfig(user_prompt="hi")
|
||||
with patch("requests.post") as mock_post:
|
||||
mock_resp = MagicMock(status_code=200)
|
||||
mock_resp.json.return_value = {"unexpected": True}
|
||||
mock_post.return_value = mock_resp
|
||||
try:
|
||||
chat_completion_text(cfg, prompt_cfg)
|
||||
assert False, "expected HttpError"
|
||||
except HttpError:
|
||||
pass
|
||||
|
||||
|
||||
def test_wait_until_ready_succeeds_immediately():
|
||||
cfg = make_cfg(timeout=5, poll_interval=0.01)
|
||||
probe = PromptConfig(user_prompt="ping", max_tokens=1)
|
||||
with patch("llamacppctl.http_ops.chat_completion") as mock_chat:
|
||||
mock_chat.return_value = MagicMock(status_code=200)
|
||||
assert wait_until_ready(cfg, probe) is True
|
||||
|
||||
|
||||
def test_wait_until_ready_times_out():
|
||||
cfg = make_cfg(timeout=0, poll_interval=0.01)
|
||||
probe = PromptConfig(user_prompt="ping", max_tokens=1)
|
||||
with patch("llamacppctl.http_ops.chat_completion") as mock_chat:
|
||||
mock_chat.return_value = MagicMock(status_code=500)
|
||||
assert wait_until_ready(cfg, probe) is False
|
||||
Loading…
Add table
Add a link
Reference in a new issue