2026-10-26 16:59:44 +08:00
|
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
|
#
|
|
|
|
|
|
# Llama 4 Scout / Maverick 服务验证脚本
|
|
|
|
|
|
#
|
|
|
|
|
|
# 默认行为:
|
|
|
|
|
|
# 1. 如果 docker 可用,自动找正在运行的 llama4-bi150 容器;
|
|
|
|
|
|
# 2. 从容器挂载/环境变量或 MODEL_HOST_DIR 推断模型目录、端口、served name;
|
|
|
|
|
|
# 3. 如果模型目录可读,调用 detect_model.py 自动判断 Scout / Maverick;
|
|
|
|
|
|
# 4. 依次检查 /health、/v1/models、/v1/chat/completions;
|
|
|
|
|
|
# 5. 可选 --with-reasoning 和 --with-image。
|
|
|
|
|
|
#
|
|
|
|
|
|
# 常用:
|
|
|
|
|
|
# ./verify.sh
|
|
|
|
|
|
# ./verify.sh --model-dir /mnt/disk0/models/LLM-Research/Llama-4-Maverick-17B-128E-Instruct-FP8 --with-image
|
|
|
|
|
|
# ./verify.sh --container llama4-scout --with-reasoning
|
|
|
|
|
|
# ./verify.sh --base-url http://127.0.0.1:8092 --served-name llama4-maverick
|
|
|
|
|
|
#
|
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
|
|
|
|
cd "$(dirname "$0")"
|
|
|
|
|
|
|
|
|
|
|
|
MODEL_DIR="${MODEL_HOST_DIR:-}"
|
|
|
|
|
|
CONTAINER="${CONTAINER:-}"
|
|
|
|
|
|
PORT="${PORT:-}"
|
|
|
|
|
|
SERVED_NAME="${SERVED_NAME:-}"
|
|
|
|
|
|
BASE_URL="${BASE_URL:-}"
|
|
|
|
|
|
TIMEOUT="${TIMEOUT:-180}"
|
|
|
|
|
|
WITH_IMAGE=0
|
|
|
|
|
|
WITH_REASONING=0
|
|
|
|
|
|
SKIP_CHAT=0
|
|
|
|
|
|
KIND="unknown"
|
|
|
|
|
|
|
|
|
|
|
|
usage() {
|
|
|
|
|
|
cat >&2 <<'EOF'
|
|
|
|
|
|
用法: ./verify.sh [options]
|
|
|
|
|
|
|
|
|
|
|
|
--model-dir PATH 模型目录(含 config.json),用于自动判断 Scout/Maverick
|
|
|
|
|
|
--container NAME 运行中的容器名;默认自动查找 llama4-bi150 容器
|
|
|
|
|
|
--port PORT 服务端口;默认从模型/容器推断
|
|
|
|
|
|
--served-name NAME OpenAI API 模型名;默认从模型/容器推断
|
|
|
|
|
|
--base-url URL 直接指定服务地址,例如 http://127.0.0.1:8092
|
|
|
|
|
|
--timeout SEC 等待和请求超时,默认 180
|
|
|
|
|
|
--with-reasoning 额外跑一个简单推理题(期望 27)
|
|
|
|
|
|
--with-image 额外跑图片识别(仅 Maverick 有意义)
|
|
|
|
|
|
--skip-chat 只检查 /health 和 /v1/models
|
|
|
|
|
|
-h, --help 显示帮助
|
|
|
|
|
|
|
|
|
|
|
|
环境变量也可以设置:MODEL_HOST_DIR CONTAINER PORT SERVED_NAME BASE_URL TIMEOUT
|
|
|
|
|
|
EOF
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
|
|
|
|
case "$1" in
|
|
|
|
|
|
--model-dir) MODEL_DIR="${2:?--model-dir 需要路径}"; shift 2 ;;
|
|
|
|
|
|
--container) CONTAINER="${2:?--container 需要名称}"; shift 2 ;;
|
|
|
|
|
|
--port) PORT="${2:?--port 需要端口}"; shift 2 ;;
|
|
|
|
|
|
--served-name) SERVED_NAME="${2:?--served-name 需要名称}"; shift 2 ;;
|
|
|
|
|
|
--base-url) BASE_URL="${2:?--base-url 需要 URL}"; shift 2 ;;
|
|
|
|
|
|
--timeout) TIMEOUT="${2:?--timeout 需要秒数}"; shift 2 ;;
|
|
|
|
|
|
--with-image) WITH_IMAGE=1; shift ;;
|
|
|
|
|
|
--with-reasoning) WITH_REASONING=1; shift ;;
|
|
|
|
|
|
--skip-chat) SKIP_CHAT=1; shift ;;
|
|
|
|
|
|
-h|--help) usage; exit 0 ;;
|
|
|
|
|
|
*) echo "ERROR: 未知参数: $1" >&2; usage; exit 2 ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
|
|
fail() { echo "FAIL: $*" >&2; exit 1; }
|
|
|
|
|
|
pass() { echo "PASS: $*"; }
|
|
|
|
|
|
|
|
|
|
|
|
command -v curl >/dev/null 2>&1 || fail "缺少 curl"
|
|
|
|
|
|
command -v python3 >/dev/null 2>&1 || fail "缺少 python3"
|
|
|
|
|
|
|
|
|
|
|
|
TMP="$(mktemp -d)"
|
|
|
|
|
|
trap 'rm -rf "${TMP}"' EXIT
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------- container
|
|
|
|
|
|
if [[ -z "${CONTAINER}" ]] && command -v docker >/dev/null 2>&1; then
|
|
|
|
|
|
CONTAINER="$(docker ps --format '{{.Names}} {{.Image}}' 2>/dev/null \
|
|
|
|
|
|
| awk '/llama4-bi150|llama4-scout|llama4-maverick/ {print $1; exit}' || true)"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
container_env() {
|
|
|
|
|
|
local name="$1" key="$2"
|
|
|
|
|
|
docker inspect "${name}" --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null \
|
|
|
|
|
|
| awk -F= -v key="${key}" '$1 == key { sub(/^[^=]*=/, ""); print; exit }' || true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if [[ -n "${CONTAINER}" ]] && command -v docker >/dev/null 2>&1; then
|
|
|
|
|
|
if [[ -z "${MODEL_DIR}" ]]; then
|
|
|
|
|
|
MODEL_DIR="$(docker inspect "${CONTAINER}" --format '{{json .Mounts}}' 2>/dev/null \
|
|
|
|
|
|
| python3 -c '
|
|
|
|
|
|
import json, sys
|
|
|
|
|
|
try:
|
|
|
|
|
|
mounts = json.load(sys.stdin)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
sys.exit(0)
|
|
|
|
|
|
for m in mounts:
|
|
|
|
|
|
if m.get("Destination") == "/models/llama4":
|
|
|
|
|
|
print(m.get("Source", ""))
|
|
|
|
|
|
break
|
|
|
|
|
|
' || true)"
|
|
|
|
|
|
fi
|
|
|
|
|
|
[[ -z "${PORT}" ]] && PORT="$(container_env "${CONTAINER}" PORT)"
|
|
|
|
|
|
[[ -z "${SERVED_NAME}" ]] && SERVED_NAME="$(container_env "${CONTAINER}" SERVED_NAME)"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------- model dir
|
2026-10-26 17:01:28 +08:00
|
|
|
|
SCOUT_DEFAULT="/mnt/disk0/models/LLM-Research/Llama-4-Scout-17B-16E-Instruct"
|
|
|
|
|
|
MAVERICK_DEFAULT="/mnt/disk0/models/LLM-Research/Llama-4-Maverick-17B-128E-Instruct-FP8"
|
|
|
|
|
|
|
|
|
|
|
|
# 没有 docker 时(例如在 ugly_hopper 内运行),按端口探测正在运行的服务。
|
|
|
|
|
|
if [[ -z "${CONTAINER}" && -z "${PORT}" && -z "${MODEL_DIR}" && -z "${BASE_URL}" ]]; then
|
|
|
|
|
|
for candidate_port in 8091 8092; do
|
|
|
|
|
|
if curl -fsS --max-time 2 "http://127.0.0.1:${candidate_port}/health" >/dev/null 2>&1; then
|
|
|
|
|
|
PORT="${candidate_port}"
|
2026-10-26 16:59:44 +08:00
|
|
|
|
break
|
|
|
|
|
|
fi
|
|
|
|
|
|
done
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
2026-10-26 17:01:28 +08:00
|
|
|
|
# 从 served name 反推模型目录(方便 --base-url + --served-name 的用法)。
|
|
|
|
|
|
if [[ -z "${MODEL_DIR}" && -n "${SERVED_NAME}" ]]; then
|
|
|
|
|
|
case "${SERVED_NAME}" in
|
|
|
|
|
|
*maverick*)
|
|
|
|
|
|
if [[ -f "${MAVERICK_DEFAULT}/config.json" ]]; then
|
|
|
|
|
|
MODEL_DIR="${MAVERICK_DEFAULT}"
|
|
|
|
|
|
fi
|
|
|
|
|
|
;;
|
|
|
|
|
|
*scout*)
|
|
|
|
|
|
if [[ -f "${SCOUT_DEFAULT}/config.json" ]]; then
|
|
|
|
|
|
MODEL_DIR="${SCOUT_DEFAULT}"
|
|
|
|
|
|
fi
|
|
|
|
|
|
;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
if [[ -z "${MODEL_DIR}" && -z "${BASE_URL}" ]]; then
|
|
|
|
|
|
if [[ "${PORT}" == "8092" && -f "${MAVERICK_DEFAULT}/config.json" ]]; then
|
|
|
|
|
|
MODEL_DIR="${MAVERICK_DEFAULT}"
|
|
|
|
|
|
elif [[ -f "${SCOUT_DEFAULT}/config.json" ]]; then
|
|
|
|
|
|
MODEL_DIR="${SCOUT_DEFAULT}"
|
|
|
|
|
|
elif [[ -f "${MAVERICK_DEFAULT}/config.json" ]]; then
|
|
|
|
|
|
MODEL_DIR="${MAVERICK_DEFAULT}"
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
2026-10-26 16:59:44 +08:00
|
|
|
|
DETECT_JSON=""
|
|
|
|
|
|
if [[ -n "${MODEL_DIR}" ]]; then
|
|
|
|
|
|
if [[ -f "${MODEL_DIR}/config.json" ]]; then
|
|
|
|
|
|
DETECT_JSON="$(python3 detect_model.py "${MODEL_DIR}" --json)" || {
|
|
|
|
|
|
echo "ERROR: 无法识别模型: ${MODEL_DIR}" >&2
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
}
|
|
|
|
|
|
elif [[ -n "${CONTAINER}" ]] && command -v docker >/dev/null 2>&1; then
|
|
|
|
|
|
# 宿主机路径不可读时,退回到容器内检测
|
|
|
|
|
|
DETECT_JSON="$(docker exec "${CONTAINER}" python3 /opt/llama4/detect_model.py /models/llama4 --json 2>/dev/null || true)"
|
|
|
|
|
|
else
|
|
|
|
|
|
fail "模型目录缺少 config.json: ${MODEL_DIR}"
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
if [[ -n "${DETECT_JSON}" ]]; then
|
|
|
|
|
|
KIND="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["kind"])' <<< "${DETECT_JSON}")"
|
|
|
|
|
|
DETECTED_PORT="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["port"])' <<< "${DETECT_JSON}")"
|
|
|
|
|
|
DETECTED_NAME="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["served_name"])' <<< "${DETECT_JSON}")"
|
|
|
|
|
|
[[ -z "${PORT}" ]] && PORT="${DETECTED_PORT}"
|
|
|
|
|
|
[[ -z "${SERVED_NAME}" ]] && SERVED_NAME="${DETECTED_NAME}"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
PORT="${PORT:-8091}"
|
|
|
|
|
|
SERVED_NAME="${SERVED_NAME:-llama4}"
|
|
|
|
|
|
BASE_URL="${BASE_URL:-http://127.0.0.1:${PORT}}"
|
|
|
|
|
|
BASE_URL="${BASE_URL%/}"
|
|
|
|
|
|
|
|
|
|
|
|
echo "=== Llama 4 verification ==="
|
|
|
|
|
|
echo "container : ${CONTAINER:-<none>}"
|
|
|
|
|
|
echo "model dir : ${MODEL_DIR:-<not detected>}"
|
|
|
|
|
|
echo "kind : ${KIND}"
|
|
|
|
|
|
echo "base URL : ${BASE_URL}"
|
|
|
|
|
|
echo "model name: ${SERVED_NAME}"
|
|
|
|
|
|
echo
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------- health
|
|
|
|
|
|
echo "==> waiting for /health (up to ${TIMEOUT}s)"
|
|
|
|
|
|
HEALTH_OK=0
|
|
|
|
|
|
DEADLINE=$((SECONDS + TIMEOUT))
|
|
|
|
|
|
while (( SECONDS < DEADLINE )); do
|
|
|
|
|
|
if curl -fsS --max-time 5 "${BASE_URL}/health" > "${TMP}/health.txt" 2>/dev/null; then
|
|
|
|
|
|
HEALTH_OK=1
|
|
|
|
|
|
break
|
|
|
|
|
|
fi
|
|
|
|
|
|
sleep 2
|
|
|
|
|
|
done
|
|
|
|
|
|
if [[ "${HEALTH_OK}" != "1" ]]; then
|
|
|
|
|
|
fail "health check 超时: ${BASE_URL}/health"
|
|
|
|
|
|
fi
|
|
|
|
|
|
HEALTH="$(cat "${TMP}/health.txt" 2>/dev/null || true)"
|
|
|
|
|
|
pass "health: ${HEALTH:-OK}"
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------- models
|
|
|
|
|
|
echo "==> /v1/models"
|
|
|
|
|
|
curl -fsS --max-time "${TIMEOUT}" "${BASE_URL}/v1/models" > "${TMP}/models.json" \
|
|
|
|
|
|
|| fail "GET /v1/models 失败"
|
|
|
|
|
|
python3 - "${TMP}/models.json" "${SERVED_NAME}" <<'PY'
|
|
|
|
|
|
import json
|
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
|
|
data = json.load(open(sys.argv[1]))
|
|
|
|
|
|
expected = sys.argv[2]
|
|
|
|
|
|
ids = [m.get("id") for m in data.get("data", [])]
|
|
|
|
|
|
if expected not in ids:
|
|
|
|
|
|
print(f"FAIL: /v1/models 中没有 {expected!r}; 实际: {ids}", file=sys.stderr)
|
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
for m in data.get("data", []):
|
|
|
|
|
|
if m.get("id") == expected:
|
|
|
|
|
|
print(f"PASS: model={expected}, max_model_len={m.get('max_model_len')}")
|
|
|
|
|
|
break
|
|
|
|
|
|
PY
|
|
|
|
|
|
|
|
|
|
|
|
if [[ "${SKIP_CHAT}" == "1" ]]; then
|
|
|
|
|
|
echo
|
|
|
|
|
|
echo "All checks passed (chat skipped)."
|
|
|
|
|
|
exit 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------- chat
|
|
|
|
|
|
echo "==> /v1/chat/completions"
|
|
|
|
|
|
python3 - "${SERVED_NAME}" > "${TMP}/chat.json" <<'PY'
|
|
|
|
|
|
import json
|
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
|
|
print(json.dumps({
|
|
|
|
|
|
"model": sys.argv[1],
|
|
|
|
|
|
"messages": [{"role": "user", "content": "你好,请用一句中文介绍你自己。"}],
|
|
|
|
|
|
"max_tokens": 64,
|
|
|
|
|
|
"temperature": 0,
|
|
|
|
|
|
}, ensure_ascii=False))
|
|
|
|
|
|
PY
|
|
|
|
|
|
curl -fsS --max-time "${TIMEOUT}" \
|
|
|
|
|
|
-H 'Content-Type: application/json' \
|
|
|
|
|
|
--data-binary @"${TMP}/chat.json" \
|
|
|
|
|
|
"${BASE_URL}/v1/chat/completions" > "${TMP}/chat_resp.json" \
|
|
|
|
|
|
|| fail "chat completion 失败"
|
|
|
|
|
|
python3 - "${TMP}/chat_resp.json" <<'PY'
|
|
|
|
|
|
import json
|
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
|
|
data = json.load(open(sys.argv[1]))
|
|
|
|
|
|
try:
|
|
|
|
|
|
content = data["choices"][0]["message"]["content"]
|
|
|
|
|
|
except (KeyError, IndexError) as exc:
|
|
|
|
|
|
print(f"FAIL: unexpected chat response: {data}", file=sys.stderr)
|
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
if isinstance(content, list):
|
|
|
|
|
|
content = " ".join(
|
|
|
|
|
|
part.get("text", "") for part in content if isinstance(part, dict)
|
|
|
|
|
|
)
|
|
|
|
|
|
content = (content or "").strip()
|
|
|
|
|
|
if not content:
|
|
|
|
|
|
print("FAIL: chat response content is empty", file=sys.stderr)
|
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
print(f"PASS: chat: {content[:200]}")
|
|
|
|
|
|
PY
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------- reasoning
|
|
|
|
|
|
if [[ "${WITH_REASONING}" == "1" ]]; then
|
|
|
|
|
|
echo "==> reasoning"
|
|
|
|
|
|
python3 - "${SERVED_NAME}" > "${TMP}/reason.json" <<'PY'
|
|
|
|
|
|
import json
|
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
|
|
print(json.dumps({
|
|
|
|
|
|
"model": sys.argv[1],
|
|
|
|
|
|
"messages": [{"role": "user", "content": "3 个人 3 天喝 3 桶水,9 个人 9 天喝几桶水?只给最终数字。"}],
|
|
|
|
|
|
"max_tokens": 16,
|
|
|
|
|
|
"temperature": 0,
|
|
|
|
|
|
}, ensure_ascii=False))
|
|
|
|
|
|
PY
|
|
|
|
|
|
curl -fsS --max-time "${TIMEOUT}" \
|
|
|
|
|
|
-H 'Content-Type: application/json' \
|
|
|
|
|
|
--data-binary @"${TMP}/reason.json" \
|
|
|
|
|
|
"${BASE_URL}/v1/chat/completions" > "${TMP}/reason_resp.json" \
|
|
|
|
|
|
|| fail "reasoning 请求失败"
|
|
|
|
|
|
python3 - "${TMP}/reason_resp.json" <<'PY'
|
|
|
|
|
|
import json
|
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
|
|
data = json.load(open(sys.argv[1]))
|
|
|
|
|
|
content = data["choices"][0]["message"].get("content", "")
|
|
|
|
|
|
if isinstance(content, list):
|
|
|
|
|
|
content = " ".join(
|
|
|
|
|
|
part.get("text", "") for part in content if isinstance(part, dict)
|
|
|
|
|
|
)
|
|
|
|
|
|
content = (content or "").strip()
|
|
|
|
|
|
if "27" not in content:
|
|
|
|
|
|
print(f"FAIL: expected 27, got: {content!r}", file=sys.stderr)
|
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
print(f"PASS: reasoning: {content[:100]}")
|
|
|
|
|
|
PY
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------- image
|
|
|
|
|
|
if [[ "${WITH_IMAGE}" == "1" ]]; then
|
|
|
|
|
|
if [[ "${KIND}" != "maverick" && "${KIND}" != "unknown" ]]; then
|
|
|
|
|
|
echo "SKIP: image test 仅适用于 Maverick(当前识别为 ${KIND})"
|
|
|
|
|
|
else
|
|
|
|
|
|
echo "==> image (336x336 red PNG)"
|
|
|
|
|
|
python3 - "${TMP}/red.b64" <<'PY'
|
|
|
|
|
|
import base64
|
|
|
|
|
|
import struct
|
|
|
|
|
|
import sys
|
|
|
|
|
|
import zlib
|
|
|
|
|
|
|
|
|
|
|
|
w = h = 336
|
|
|
|
|
|
row = bytes((220, 20, 20)) * w
|
|
|
|
|
|
raw = b"".join(b"\x00" + row for _ in range(h))
|
|
|
|
|
|
|
|
|
|
|
|
def chunk(kind, data):
|
|
|
|
|
|
return (
|
|
|
|
|
|
struct.pack(">I", len(data))
|
|
|
|
|
|
+ kind
|
|
|
|
|
|
+ data
|
|
|
|
|
|
+ struct.pack(">I", zlib.crc32(kind + data) & 0xFFFFFFFF)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
png = (
|
|
|
|
|
|
b"\x89PNG\r\n\x1a\n"
|
|
|
|
|
|
+ chunk(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, 2, 0, 0, 0))
|
|
|
|
|
|
+ chunk(b"IDAT", zlib.compress(raw, 9))
|
|
|
|
|
|
+ chunk(b"IEND", b"")
|
|
|
|
|
|
)
|
|
|
|
|
|
open(sys.argv[1], "w").write(base64.b64encode(png).decode())
|
|
|
|
|
|
PY
|
|
|
|
|
|
python3 - "${SERVED_NAME}" "${TMP}/red.b64" > "${TMP}/image.json" <<'PY'
|
|
|
|
|
|
import json
|
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
|
|
b64 = open(sys.argv[2]).read().strip()
|
|
|
|
|
|
payload = {
|
|
|
|
|
|
"model": sys.argv[1],
|
|
|
|
|
|
"messages": [{
|
|
|
|
|
|
"role": "user",
|
|
|
|
|
|
"content": [
|
|
|
|
|
|
{"type": "text", "text": "这张图片主要是什么颜色?只回答颜色。"},
|
|
|
|
|
|
{"type": "image_url", "image_url": {"url": "data:image/png;base64," + b64}},
|
|
|
|
|
|
],
|
|
|
|
|
|
}],
|
|
|
|
|
|
"max_tokens": 32,
|
|
|
|
|
|
"temperature": 0,
|
|
|
|
|
|
}
|
|
|
|
|
|
print(json.dumps(payload, ensure_ascii=False))
|
|
|
|
|
|
PY
|
|
|
|
|
|
curl -fsS --max-time "${TIMEOUT}" \
|
|
|
|
|
|
-H 'Content-Type: application/json' \
|
|
|
|
|
|
--data-binary @"${TMP}/image.json" \
|
|
|
|
|
|
"${BASE_URL}/v1/chat/completions" > "${TMP}/image_resp.json" \
|
|
|
|
|
|
|| fail "image 请求失败"
|
|
|
|
|
|
python3 - "${TMP}/image_resp.json" <<'PY'
|
|
|
|
|
|
import json
|
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
|
|
data = json.load(open(sys.argv[1]))
|
|
|
|
|
|
content = data["choices"][0]["message"].get("content", "")
|
|
|
|
|
|
if isinstance(content, list):
|
|
|
|
|
|
content = " ".join(
|
|
|
|
|
|
part.get("text", "") for part in content if isinstance(part, dict)
|
|
|
|
|
|
)
|
|
|
|
|
|
content = (content or "").strip()
|
|
|
|
|
|
if not content:
|
|
|
|
|
|
print("FAIL: image response content is empty", file=sys.stderr)
|
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
lower = content.lower()
|
|
|
|
|
|
if "红" not in lower and "red" not in lower:
|
|
|
|
|
|
print(f"FAIL: expected red, got: {content[:200]}", file=sys.stderr)
|
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
print(f"PASS: image: {content[:200]}")
|
|
|
|
|
|
PY
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
echo
|
|
|
|
|
|
echo "All checks passed for ${KIND} at ${BASE_URL}."
|