Skill: carol-gutianle/modelready (version 1.0.0)

--- name: modelready description: Start using a local or Hugging Face model instantly, directly from chat. metadata: {"openclaw":{"requires":{"bins":["bash", "curl"]}, "env": ["URL"]}} --- # ModelReady ModelReady lets you **start using a local or Hugging Face model immediately**, without leaving clawdbot. It turns a model into a running, OpenAI-compatible endpoint and allows you to chat with it directly from a conversation. ## When to use Use this skill when you want to: - Quickly start using a local or Hugging Face model - Chat with a locally running model - Test or interact with a model directly from chat ## Commands ### Start a model server ```text /modelready start repo=<path-or-hf-repo> port=<port> [tp=<n>] [dtype=<dtype>] ```` Examples: ```text /modelready start repo=Qwen/Qwen2.5-7B-Instruct port=19001 /modelready start repo=/home/user/models/Qwen-2.5 port=8010 tp=4 dtype=bfloat16 ``` ### Chat with a running model ```text /modelready chat port=<port> text="<message>" ``` Example: ```text /modelready chat port=8010 text="hello" ``` ### Check status or stop the server ```text /modelready status port=<port> /modelready stop port=<port> ``` ### Set default host or port ```text /modelready set_ip ip=<host> /modelready set_port port=<port> ``` ## Notes * The model is served locally using vLLM. * The exposed endpoint follows the OpenAI API format. * The server must be started before sending chat requests.

path: tool/modelready.sh
content: #!/usr/bin/env bash
set -euo pipefail

CMD="${1:-start}"
shift || true

# parse key=val args
declare -A KV
for arg in "$@"; do
  if [[ "$arg" == *"="* ]]; then
    k="${arg%%=*}"
    v="${arg#*=}"
    KV["$k"]="$v"
  fi
done

RUN_DIR="${HOME}/.model2skill"
mkdir -p "$RUN_DIR"

# ---- persistent defaults ----
DEFAULTS_FILE="$RUN_DIR/defaults.env"

load_defaults() {
  if [[ -f "$DEFAULTS_FILE" ]]; then
    # shellcheck disable=SC1090
    source "$DEFAULTS_FILE"
  fi
}

save_defaults() {
  # write atomically
  local tmp
  tmp="$(mktemp "$RUN_DIR/defaults.env.XXXXXX")"
  {
    echo "# generated by model2skill"
    echo "DEFAULT_HOST=${DEFAULT_HOST@Q}"
    echo "DEFAULT_PORT=${DEFAULT_PORT@Q}"
  } >"$tmp"
  mv "$tmp" "$DEFAULTS_FILE"
}

load_defaults
DEFAULT_HOST="${DEFAULT_HOST:-0.0.0.0}"
DEFAULT_PORT="${DEFAULT_PORT:-19001}"
# ----------------------------

# Server-side bind host/port (what vLLM listens on)
PORT="${KV[port]:-$DEFAULT_PORT}"
HOST="${KV[host]:-$DEFAULT_HOST}"

TP="${KV[tp]:-1}"
REPO="${KV[repo]:-}"

PID_FILE="$RUN_DIR/vllm_${PORT}.pid"
LOG_FILE="$RUN_DIR/vllm_${PORT}.log"

get_ip() {
  # Linux: hostname -I ; macOS fallback: ipconfig
  if command -v hostname >/dev/null 2>&1 && hostname -I >/dev/null 2>&1; then
    hostname -I | awk '{print $1}'
  else
    ipconfig getifaddr en0 2>/dev/null \
      || ipconfig getifaddr en1 2>/dev/null \
      || echo "127.0.0.1"
  fi
}

resolve_host() {
  # 0.0.0.0 cannot be used as a client target; resolve to actual IP
  if [[ "$HOST" == "0.0.0.0" ]]; then
    get_ip
  else
    echo "$HOST"
  fi
}

set_ip() {
  local ip="${KV[ip]:-}"
  if [[ -z "$ip" ]]; then
    echo "Missing ip=..."
    echo "e.g. $0 set_ip ip=0.0.0.0"
    exit 1
  fi
  DEFAULT_HOST="$ip"
  save_defaults
  echo "OK default_host=$DEFAULT_HOST"
}

set_port() {
  local p="${KV[port]:-}"
  if [[ -z "$p" ]]; then
    echo "Missing port=..."
    echo "e.g. $0 set_port port=19001"
    exit 1
  fi
  if ! [[ "$p" =~ ^[0-9]+$ ]] || (( p < 1 || p > 65535 )); then
    echo "Invalid port=$p (must be 1-65535)"
    exit 1
  fi
  DEFAULT_PORT="$p"
  save_defaults
  echo "OK default_port=$DEFAULT_PORT"
}

status() {
  if [[ -f "$PID_FILE" ]]; then
    pid="$(cat "$PID_FILE")"
    if kill -0 "$pid" 2>/dev/null; then
      client_host="$(resolve_host)"
      echo "RUNNING pid=$pid bind=${HOST}:${PORT}"
      echo "base_url=http://${client_host}:${PORT}/v1"
      exit 0
    fi
  fi
  echo "NOT RUNNING port=$PORT"
}

stop() {
  if [[ -f "$PID_FILE" ]]; then
    pid="$(cat "$PID_FILE")"
    if kill -0 "$pid" 2>/dev/null; then
      kill "$pid" 2>/dev/null || true
      # wait up to ~10s
      for _ in $(seq 1 20); do
        kill -0 "$pid" 2>/dev/null || break
        sleep 0.5
      done
      # still alive -> force
      kill -9 "$pid" 2>/dev/null || true
    fi
    rm -f "$PID_FILE"
    echo "STOPPED port=$PORT"
  else
    echo "NO PID FILE port=$PORT"
  fi
}

logs() {
  tail -n 80 "$LOG_FILE" || true
}

start() {
  if [[ -z "$REPO" ]]; then
    echo "Missing repo=PATH_OR_HF_REPO"
    exit 1
  fi

  # already running?
  if [[ -f "$PID_FILE" ]]; then
    pid="$(cat "$PID_FILE")"
    if kill -0 "$pid" 2>/dev/null; then
      client_host="$(resolve_host)"
      echo "ALREADY RUNNING pid=$pid"
      echo "base_url=http://${client_host}:${PORT}/v1"
      exit 0
    fi
  fi

  NAME="${KV[name]:-}"
  DTYPE="${KV[dtype]:-auto}"
  MAX_NUM_SEQS="${KV[max_num_seqs]:-}"
  AUTO_TOOL="${KV[auto_tool]:-0}"
  TOOL_PARSER="${KV[tool_parser]:-}"

  # build args
  ARGS=(--model "$REPO" --host "$HOST" --port "$PORT" --tensor-parallel-size "$TP" --dtype "$DTYPE")

  if [[ -n "$NAME" ]]; then
    ARGS+=(--served-model-name "$NAME")
  fi

  if [[ -n "$MAX_NUM_SEQS" ]]; then
    ARGS+=(--max-num-seqs "$MAX_NUM_SEQS")
  fi

  if [[ "$AUTO_TOOL" == "1" ]]; then
    ARGS+=(--enable-auto-tool-choice)
  fi

  if [[ -n "$TOOL_PARSER" ]]; then
    ARGS+=(--tool-call-parser "$TOOL_PARSER")
  fi

  # OPTIONAL: passthrough extra="--foo bar --baz 1"
  EXTRA="${KV[extra]:-}"
  if [[ -n "$EXTRA" ]]; then
    # shellcheck disable=SC2206
    EXTRA_ARR=($EXTRA)
    ARGS+=("${EXTRA_ARR[@]}")
  fi

  # start server
  nohup python3 -m vllm.entrypoints.openai.api_server \
    "${ARGS[@]}" \
    >"$LOG_FILE" 2>&1 &

  echo $! > "$PID_FILE"

  # wait until ready (best-effort but honest)
  client_host="$(resolve_host)"
  ready=0
  for _ in $(seq 1 180); do
    if curl -sf "http://${client_host}:${PORT}/v1/models" >/dev/null 2>&1; then
      ready=1
      break
    fi
    sleep 1
  done

  if [[ "$ready" != "1" ]]; then
    echo "ERROR: server not ready on bind=${HOST}:${PORT}"
    echo "hint: $0 logs port=$PORT"
    exit 1
  fi

  echo "OK model=$REPO pid=$(cat "$PID_FILE") bind=${HOST}:${PORT}"
  echo "base_url=http://${client_host}:${PORT}/v1"
  echo "try: curl http://${client_host}:${PORT}/v1/models"
}

chat() {
  TEXT="${KV[text]:-}"
  if [[ -z "$TEXT" ]]; then
    echo "Missing text=..."
    exit 1
  fi

  MODEL="${KV[model]:-moderation}"
  TEMP="${KV[temp]:-0}"
  MAX_TOKENS="${KV[max_tokens]:-256}"

  CHAT_PORT="${KV[port]:-$DEFAULT_PORT}"
  CHAT_HOST="${KV[host]:-$DEFAULT_HOST}"

  # for client, 0.0.0.0 must be resolved to real IP/loopback
  if [[ "$CHAT_HOST" == "0.0.0.0" ]]; then
    CHAT_HOST="$(get_ip)"
  fi

  URL="http://${CHAT_HOST}:${CHAT_PORT}/v1/chat/completions"

CHAT_TEXT="$TEXT" CHAT_MODEL="$MODEL" CHAT_TEMP="$TEMP" CHAT_MAX_TOKENS="$MAX_TOKENS" CHAT_URL="$URL" \
python3 - <<'PY'
import json, os, re, sys, urllib.request, urllib.error

url = os.environ["CHAT_URL"]
payload = {
    "model": os.environ.get("CHAT_MODEL", "moderation"),
    "messages": [{"role": "user", "content": os.environ.get("CHAT_TEXT", "")}],
    "temperature": float(os.environ.get("CHAT_TEMP", "0")),
    "max_tokens": int(os.environ.get("CHAT_MAX_TOKENS", "256")),
}

print(url, file=sys.stderr)

req = urllib.request.Request(
    url,
    data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
    headers={"Content-Type": "application/json"},
)

try:
    import urllib.request

    opener = urllib.request.build_opener(
        urllib.request.ProxyHandler({})
    )

    with opener.open(req, timeout=300) as resp:
        data = json.load(resp)

except urllib.error.HTTPError as e:
    body = e.read().decode("utf-8", errors="replace")
    print(f"[HTTPError] {e.code} {e.reason}\n{body}", file=sys.stderr)
    raise
except urllib.error.URLError as e:
    print(f"[URLError] {e.reason}", file=sys.stderr)
    raise

content = data["choices"][0]["message"]["content"]
content = re.sub(r"<think>[\s\S]*?</think>", "", content).strip()
print(f"你的模型说：{content}")
PY

}

case "$CMD" in
  set_ip) set_ip ;;
  set_port) set_port ;;
  start) start ;;
  status) status ;;
  stop) stop ;;
  logs) logs ;;
  chat) chat ;;
  *) echo "Unknown cmd: $CMD (set_ip|set_port|start|status|stop|logs|chat)"; exit 1 ;;
esac


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: suspicious | SkillSpector: suspicious (MEDIUM)