mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
The role test asked for /v1/models on 127.0.0.1 from inside the container, the one address that still answered while the server was bound to loopback alone. It stayed green and the failure surfaced one role later as a 500 in litellm's warm-up. Step 3 now resolves the container's first IPv4 through hostname -I and queries that address, which a loopback-only bind refuses. It reads the interfaces directly, so it needs neither the service name nor swarm's route back through the service VIP, and compose and swarm take the same path. When the models never appear, one more request tells an unreachable server apart from an unindexed store. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
115 lines
3.9 KiB
Bash
115 lines
3.9 KiB
Bash
#!/usr/bin/env bash
|
|
# shellcheck shell=bash
|
|
#
|
|
# Env (rendered into test.env from templates/test.env.j2):
|
|
# LMSTUDIO_CONTAINER resolved container id on this node
|
|
# LMSTUDIO_PORT the daemon's HTTP port
|
|
# LMSTUDIO_EXPECTED_MODELS JSON array of model names the daemon must serve
|
|
# LMSTUDIO_EXPECTED_BLOBS JSON array of <repo>/<file> paths under models/
|
|
# LMSTUDIO_EXPECTED_DIGESTS JSON array of sha256, aligned with the blobs
|
|
# LMSTUDIO_MODELS_DIR the models mount inside the container
|
|
# RETRIES attempts (default 30)
|
|
# SLEEP_SECONDS wait between (default 10)
|
|
|
|
set -uo pipefail
|
|
|
|
RETRIES="${RETRIES:-30}"
|
|
SLEEP_SECONDS="${SLEEP_SECONDS:-10}"
|
|
MODELS_DIR="${LMSTUDIO_MODELS_DIR}"
|
|
|
|
[ -n "${LMSTUDIO_CONTAINER}" ] || {
|
|
echo "[FATAL] LMSTUDIO_CONTAINER unset; the harness did not resolve a container" >&2
|
|
exit 2
|
|
}
|
|
|
|
in_container() {
|
|
container exec "${LMSTUDIO_CONTAINER}" "$@"
|
|
}
|
|
|
|
json_items() {
|
|
python3 -c 'import json,sys; [print(x) for x in json.loads(sys.argv[1])]' "$1"
|
|
}
|
|
|
|
server_url=""
|
|
|
|
serves_model() {
|
|
in_container curl -fsS --connect-timeout 5 --max-time 30 "${server_url}/v1/models" 2>/dev/null |
|
|
python3 -c 'import json,sys; d=json.load(sys.stdin); print("\n".join(m.get("id","") for m in d.get("data",[])))' 2>/dev/null |
|
|
grep -qxF "$1"
|
|
}
|
|
|
|
failed=0
|
|
|
|
echo "=== 1. the CLI authenticates against its own daemon ==="
|
|
attempt=1
|
|
auth_out=""
|
|
while [ "${attempt}" -le "${RETRIES}" ]; do
|
|
if auth_out="$(in_container /app/.bundle/lms ls 2>&1)"; then
|
|
echo "[OK] lms ls authenticated"
|
|
break
|
|
fi
|
|
if printf '%s' "${auth_out}" | grep -q 'authPacket'; then
|
|
echo "[FAIL] the daemon rejected its own CLI: $(printf '%s' "${auth_out}" | grep -m1 authPacket)" >&2
|
|
echo " a second daemon shares /root/.lmstudio and rewrote .internal/lms-key-2" >&2
|
|
exit 1
|
|
fi
|
|
attempt=$((attempt + 1))
|
|
[ "${attempt}" -le "${RETRIES}" ] && sleep "${SLEEP_SECONDS}"
|
|
done
|
|
if [ "${attempt}" -gt "${RETRIES}" ]; then
|
|
echo "[FAIL] lms ls never succeeded after ${RETRIES} attempts: ${auth_out}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "=== 2. every declared blob is in place with its declared digest ==="
|
|
mapfile -t blobs < <(json_items "${LMSTUDIO_EXPECTED_BLOBS}")
|
|
mapfile -t digests < <(json_items "${LMSTUDIO_EXPECTED_DIGESTS}")
|
|
for i in "${!blobs[@]}"; do
|
|
path="${MODELS_DIR}/${blobs[$i]}"
|
|
if ! in_container test -f "${path}"; then
|
|
echo "[FAIL] ${blobs[$i]} is not in the model store" >&2
|
|
failed=1
|
|
continue
|
|
fi
|
|
actual="$(in_container sha256sum "${path}" 2>/dev/null | awk '{print $1}')"
|
|
if [ "${actual}" != "${digests[$i]}" ]; then
|
|
echo "[FAIL] ${blobs[$i]} hashes to ${actual}, declared ${digests[$i]}" >&2
|
|
failed=1
|
|
continue
|
|
fi
|
|
echo "[OK] ${blobs[$i]} present and matching"
|
|
done
|
|
|
|
echo "=== 3. the daemon serves every declared model on its network address ==="
|
|
address="$(in_container hostname -I | tr ' ' '\n' | grep -m1 -E '^[0-9]+(\.[0-9]+){3}$')"
|
|
if [ -z "${address}" ]; then
|
|
echo "[FAIL] hostname -I reported no IPv4 address inside the container" >&2
|
|
exit 1
|
|
fi
|
|
server_url="http://${address}:${LMSTUDIO_PORT}"
|
|
mapfile -t models < <(json_items "${LMSTUDIO_EXPECTED_MODELS}")
|
|
for model in "${models[@]}"; do
|
|
attempt=1
|
|
while [ "${attempt}" -le "${RETRIES}" ]; do
|
|
if serves_model "${model}"; then
|
|
echo "[OK] ${model} is served"
|
|
break
|
|
fi
|
|
attempt=$((attempt + 1))
|
|
[ "${attempt}" -le "${RETRIES}" ] && sleep "${SLEEP_SECONDS}"
|
|
done
|
|
if [ "${attempt}" -gt "${RETRIES}" ]; then
|
|
if ! in_container curl -fsS --connect-timeout 5 --max-time 30 -o /dev/null "${server_url}/v1/models"; then
|
|
echo "[FAIL] ${server_url} does not answer" >&2
|
|
echo " other containers cannot reach the server; check that LMS_SERVER_HOST binds it beyond loopback" >&2
|
|
else
|
|
echo "[FAIL] ${model} is on disk but /v1/models never listed it" >&2
|
|
echo " the daemon did not index the store it was given at startup" >&2
|
|
fi
|
|
failed=1
|
|
fi
|
|
done
|
|
|
|
[ "${failed}" -eq 0 ] || exit 1
|
|
echo "[OK] svc-ai-lmstudio serves every declared model from the seeded store"
|