Files
2026-04-11 09:45:12 -05:00

223 lines
6.5 KiB
Bash

#!/usr/bin/env bash
# Verify Ollama embeddings endpoint with selected model.
# Checks: model exists in Ollama → endpoint reachable → valid embedding response.
set -euo pipefail
MODEL=""
BASE_URL=""
CONFIG_PATH="${OPENCLAW_CONFIG_PATH:-${HOME}/.openclaw/openclaw.json}"
VERBOSE=0
usage() {
cat <<'EOF'
Usage:
verify.sh [--model <id>] [--base-url <url>] [--openclaw-config <path>] [--verbose]
Verifies that the configured Ollama embeddings endpoint is working correctly.
Behavior:
- If --model is omitted, reads memorySearch.model from OpenClaw config.
- If --base-url is omitted, reads memorySearch.remote.baseUrl from config,
then defaults to http://127.0.0.1:11434/v1/
- Checks: (1) model exists in Ollama, (2) endpoint returns valid embedding.
- Use --verbose to dump raw API response on failure.
EOF
}
while [ $# -gt 0 ]; do
case "$1" in
--model) MODEL="$2"; shift 2 ;;
--base-url) BASE_URL="$2"; shift 2 ;;
--openclaw-config) CONFIG_PATH="$2"; shift 2 ;;
--verbose) VERBOSE=1; shift ;;
--help|-h) usage; exit 0 ;;
*) echo "Unknown option: $1"; usage; exit 1 ;;
esac
done
require_cmd() {
command -v "$1" >/dev/null 2>&1 || {
echo "ERROR: '$1' not found in PATH."
exit 1
}
}
# Normalize model name: add :latest if no tag present.
normalize_model() {
local m="$1"
if [[ "$m" != *:* ]]; then
echo "${m}:latest"
else
echo "$m"
fi
}
require_cmd node
require_cmd curl
# ── Read config if needed ────────────────────────────────────────────────────
if [ -z "$MODEL" ] || [ -z "$BASE_URL" ]; then
export CONFIG_PATH
MAP_OUTPUT="$(node -e '
const fs = require("fs");
const p = process.env.CONFIG_PATH;
const CANDIDATES = [
["agents","defaults","memorySearch"],
["memorySearch"],
["agents","memorySearch"],
["agents","defaults","memory","search"],
["memory","search"],
];
function getAt(obj, path) {
let cur = obj;
for (const k of path) {
if (!cur || typeof cur !== "object" || !(k in cur)) return undefined;
cur = cur[k];
}
return cur;
}
function resolveMs(cfg) {
const canonical = getAt(cfg, CANDIDATES[0]);
if (canonical && typeof canonical === "object" && !Array.isArray(canonical)) return canonical;
for (const p of CANDIDATES.slice(1)) {
const v = getAt(cfg, p);
if (v && typeof v === "object" && !Array.isArray(v)) return v;
}
return {};
}
let cfg = {};
try { cfg = JSON.parse(fs.readFileSync(p, "utf8")); } catch (_) {}
const ms = resolveMs(cfg);
const model = ms.model || "";
const base = (ms?.remote?.baseUrl || "http://127.0.0.1:11434/v1/").trim();
console.log(model);
console.log(base);
')"
CFG_MODEL="$(printf "%s\n" "$MAP_OUTPUT" | sed -n '1p')"
CFG_BASE_URL="$(printf "%s\n" "$MAP_OUTPUT" | sed -n '2p')"
[ -z "$MODEL" ] && MODEL="$CFG_MODEL"
[ -z "$BASE_URL" ] && BASE_URL="$CFG_BASE_URL"
fi
if [ -z "$MODEL" ]; then
echo "ERROR: Could not determine embedding model."
echo " Provide --model <id> or configure memorySearch.model in ${CONFIG_PATH}"
exit 1
fi
# Normalize model tag
MODEL="$(normalize_model "$MODEL")"
# Normalize URL to .../v1 and call /embeddings
BASE_URL="${BASE_URL%/}"
if [[ "$BASE_URL" != */v1 ]]; then
BASE_URL="${BASE_URL}/v1"
fi
EMBED_URL="${BASE_URL}/embeddings"
echo "Checking Ollama embeddings:"
echo " URL: ${EMBED_URL}"
echo " Model: ${MODEL}"
# ── Step 1: Check model exists in Ollama ─────────────────────────────────────
echo ""
echo " [1/2] Checking model availability in Ollama..."
if command -v ollama >/dev/null 2>&1; then
if ! ollama list 2>/dev/null | awk 'NR>1{print $1}' | grep -qE "^(${MODEL}|${MODEL%%:*})$" 2>/dev/null; then
echo " WARNING: model '${MODEL}' not found in 'ollama list'."
echo " The model may not be pulled. Try: ollama pull ${MODEL%%:*}"
echo ""
echo " Continuing with endpoint check anyway..."
else
echo " Model '${MODEL}' found in Ollama."
fi
else
echo " NOTE: 'ollama' CLI not in PATH; skipping model existence check."
fi
# ── Step 2: Call embeddings endpoint ─────────────────────────────────────────
echo " [2/2] Calling embeddings endpoint..."
PAYLOAD=$(cat <<EOF
{"model":"${MODEL}","input":"openclaw memory embeddings health check"}
EOF
)
HTTP_CODE=""
RESP=""
# Capture HTTP status and response body without mixing stderr into status code.
TMP_BODY="$(mktemp)"
TMP_ERR="$(mktemp)"
set +e
HTTP_CODE="$(curl -sS -o "$TMP_BODY" -w "%{http_code}" \
-H "Content-Type: application/json" \
-d "$PAYLOAD" \
"$EMBED_URL" 2>"$TMP_ERR")"
CURL_STATUS=$?
set -e
RESP="$(cat "$TMP_BODY")"
CURL_ERR="$(cat "$TMP_ERR")"
rm -f "$TMP_BODY" "$TMP_ERR"
if [ "$CURL_STATUS" -ne 0 ]; then
echo " ERROR: curl failed to reach ${EMBED_URL}"
echo " Is Ollama running? Check: curl http://127.0.0.1:11434/api/tags"
if [ "$VERBOSE" -eq 1 ] && [ -n "$CURL_ERR" ]; then
echo " curl error: ${CURL_ERR}"
fi
exit 1
fi
if [ "$HTTP_CODE" != "200" ]; then
echo " ERROR: embeddings endpoint returned HTTP ${HTTP_CODE}"
if [ "$VERBOSE" -eq 1 ] && [ -n "$RESP" ]; then
echo ""
echo " Raw response (first 2000 chars):"
echo " ${RESP:0:2000}"
fi
# Try to extract error message
if [ -n "$RESP" ]; then
ERR_MSG="$(echo "$RESP" | node -e '
let d=""; process.stdin.on("data",c=>d+=c); process.stdin.on("end",()=>{
try { const j=JSON.parse(d); console.log(j.error||j.message||""); } catch { console.log(""); }
});
' 2>/dev/null)" || true
if [ -n "$ERR_MSG" ]; then
echo " Server error: ${ERR_MSG}"
fi
fi
exit 1
fi
export RESP VERBOSE
node <<'NODEOF'
const raw = process.env.RESP || "";
const verbose = process.env.VERBOSE === "1";
let body;
try { body = JSON.parse(raw); } catch {
console.error(" ERROR: embeddings endpoint did not return valid JSON.");
if (verbose) {
console.error(" Raw response (first 2000 chars):");
console.error(" " + raw.slice(0, 2000));
}
process.exit(1);
}
const arr = body?.data?.[0]?.embedding;
if (!Array.isArray(arr) || arr.length === 0) {
console.error(" ERROR: embeddings response missing data[0].embedding.");
console.error(" Top-level keys: " + Object.keys(body).join(", "));
if (verbose) {
console.error(" Raw response (first 2000 chars):");
console.error(" " + raw.slice(0, 2000));
}
process.exit(1);
}
console.log(` OK: received embedding vector (dims=${arr.length})`);
NODEOF