#!/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 ] [--base-url ] [--openclaw-config ] [--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 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 <"$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