From 3366d6d9ec7407ce741a9ea782901def03b38dbe Mon Sep 17 00:00:00 2001 From: Norbert Date: Wed, 18 Feb 2026 17:16:35 +0000 Subject: [PATCH] Initial commit: Ollama GPU Switcher Simple web UI to toggle OpenClaw agents between work mode (qwen3 on ollama) and lab mode (groq cloud fallback), giving the lab agent exclusive GPU access. Features: - One-click mode switching - Real-time agent status - Lab model selector - Direct config file patching + gateway restart --- .gitignore | 4 + README.md | 66 ++++++++ app.py | 195 ++++++++++++++++++++++ requirements.txt | 2 + static/index.html | 410 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 677 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app.py create mode 100644 requirements.txt create mode 100644 static/index.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2396b2d --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +*.pyc +.env +venv/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..22be517 --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +# Ollama GPU Switcher + +A simple web UI to toggle OpenClaw agents between **work mode** (local ollama inference) and **lab mode** (cloud fallback), so experiments get exclusive GPU access. + +## The Problem + +With a single GPU (RTX 3090), loading different models causes VRAM swaps. When the lab agent (Eric) loads granite4 while other agents are using qwen3, both tasks fail. This tool lets you switch all non-lab agents to cloud (groq) with one click. + +## Modes + +| Mode | GPU Agents | Lab Agent | GPU Status | +|------|-----------|-----------|------------| +| ๐Ÿ› ๏ธ Work | qwen3-128k:14b (ollama) | granite4 (ollama) | Shared | +| ๐Ÿงช Lab | groq (cloud) | granite4 (ollama) | Exclusive for lab | + +## Features + +- One-click mode switching (work โ†” lab) +- Real-time agent status display +- Lab model selector (change what Eric runs) +- Auto-refresh every 30s +- Dark theme, mobile-friendly +- **No LLM involved** โ€” pure config switching via OpenClaw gateway API + +## Setup + +```bash +pip install -r requirements.txt +``` + +## Usage + +```bash +# Set your gateway token (from openclaw.json) +export OPENCLAW_GATEWAY_TOKEN="your-token-here" + +# Run on port 8585 (default) +python app.py + +# Or custom port +PORT=9090 python app.py +``` + +Then open `http://localhost:8585` in your browser. + +## Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `OPENCLAW_GATEWAY_URL` | `http://127.0.0.1:18789` | Gateway API endpoint | +| `OPENCLAW_GATEWAY_TOKEN` | *(empty)* | Gateway auth token | +| `PORT` | `8585` | Web UI port | + +## How It Works + +The app reads and patches the OpenClaw gateway config via its REST API: + +1. **Status**: `GET /api/status` โ†’ reads agent model assignments +2. **Switch**: `POST /api/switch` โ†’ patches agent models (qwen3 โ†” groq) +3. **Lab model**: `POST /api/lab-model` โ†’ changes Eric's model + +Config changes trigger an automatic gateway restart. + +## License + +MIT diff --git a/app.py b/app.py new file mode 100644 index 0000000..3722da4 --- /dev/null +++ b/app.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +""" +Ollama GPU Switcher โ€” Toggle OpenClaw agents between work mode (qwen3) and lab mode (GPU exclusive). +No LLM involved. Reads/writes openclaw.json directly, then signals the gateway to restart. +""" + +import json +import os +import signal +import subprocess +import copy +from flask import Flask, jsonify, request, send_from_directory + +app = Flask(__name__, static_folder="static") + +CONFIG_PATH = os.environ.get("OPENCLAW_CONFIG", os.path.expanduser("~/.openclaw/openclaw.json")) + +# Agents that use ollama and compete for GPU +OLLAMA_AGENTS = ["rex", "maddy", "coder", "research"] + +WORK_PRIMARY = "ollama/qwen3-128k:14b" +LAB_PRIMARY = "groq/llama-3.3-70b-versatile" + + +def read_config(): + with open(CONFIG_PATH, "r") as f: + return json.load(f) + + +def write_config(config): + with open(CONFIG_PATH, "w") as f: + json.dump(config, f, indent=2) + f.write("\n") + + +def restart_gateway(): + """Restart the openclaw gateway via CLI.""" + try: + subprocess.run(["openclaw", "gateway", "restart"], timeout=10, capture_output=True) + return True + except Exception: + # Fallback: try SIGUSR1 to the gateway process + try: + result = subprocess.run(["pgrep", "-f", "openclaw.*gateway"], capture_output=True, text=True) + if result.stdout.strip(): + pid = int(result.stdout.strip().split("\n")[0]) + os.kill(pid, signal.SIGUSR1) + return True + except Exception: + pass + return False + + +def find_agent(config, agent_id): + for agent in config.get("agents", {}).get("list", []): + if agent.get("id") == agent_id: + return agent + return None + + +def detect_mode(config): + ollama_count = 0 + groq_count = 0 + for agent_id in OLLAMA_AGENTS: + agent = find_agent(config, agent_id) + if agent: + primary = agent.get("model", {}).get("primary", "") + if "ollama/" in primary: + ollama_count += 1 + elif "groq/" in primary: + groq_count += 1 + + if ollama_count == len(OLLAMA_AGENTS): + return "work" + elif groq_count >= len(OLLAMA_AGENTS): + return "lab" + return "mixed" + + +@app.route("/") +def index(): + return send_from_directory("static", "index.html") + + +@app.route("/api/status") +def status(): + try: + config = read_config() + mode = detect_mode(config) + + agent_details = [] + for agent_id in OLLAMA_AGENTS: + agent = find_agent(config, agent_id) + if agent: + agent_details.append({ + "id": agent["id"], + "name": agent.get("name", agent["id"]), + "model": agent.get("model", {}).get("primary", "unknown"), + }) + + lab = find_agent(config, "lab") + lab_info = { + "name": lab.get("name", "Eric") if lab else "Eric", + "model": lab.get("model", {}).get("primary", "unknown") if lab else "unknown", + } + + # Subagents default + subagents_primary = ( + config.get("agents", {}) + .get("defaults", {}) + .get("subagents", {}) + .get("model", {}) + .get("primary", "unknown") + ) + + return jsonify({ + "ok": True, + "mode": mode, + "lab": lab_info, + "agents": agent_details, + "subagentsPrimary": subagents_primary, + }) + except Exception as e: + return jsonify({"ok": False, "error": str(e)}), 500 + + +@app.route("/api/switch", methods=["POST"]) +def switch(): + try: + data = request.json or {} + target_mode = data.get("mode", "work") + + if target_mode == "lab": + new_primary = LAB_PRIMARY + elif target_mode == "work": + new_primary = WORK_PRIMARY + else: + return jsonify({"ok": False, "error": f"Unknown mode: {target_mode}"}), 400 + + config = read_config() + + # Patch each agent's primary model + for agent_id in OLLAMA_AGENTS: + agent = find_agent(config, agent_id) + if agent: + if "model" not in agent: + agent["model"] = {} + agent["model"]["primary"] = new_primary + + # Patch subagents default + config.setdefault("agents", {}).setdefault("defaults", {}).setdefault("subagents", {}).setdefault("model", {}) + config["agents"]["defaults"]["subagents"]["model"]["primary"] = new_primary + + write_config(config) + restarted = restart_gateway() + + return jsonify({ + "ok": True, + "mode": target_mode, + "restarted": restarted, + }) + except Exception as e: + return jsonify({"ok": False, "error": str(e)}), 500 + + +@app.route("/api/lab-model", methods=["POST"]) +def set_lab_model(): + try: + data = request.json or {} + model = data.get("model", "") + if not model: + return jsonify({"ok": False, "error": "No model specified"}), 400 + + config = read_config() + lab = find_agent(config, "lab") + if not lab: + return jsonify({"ok": False, "error": "Lab agent not found"}), 404 + + if "model" not in lab: + lab["model"] = {} + lab["model"]["primary"] = model + + write_config(config) + restarted = restart_gateway() + + return jsonify({"ok": True, "model": model, "restarted": restarted}) + except Exception as e: + return jsonify({"ok": False, "error": str(e)}), 500 + + +if __name__ == "__main__": + port = int(os.environ.get("PORT", 8585)) + print(f"๐Ÿ”€ Ollama GPU Switcher running on http://0.0.0.0:{port}") + print(f"๐Ÿ“„ Config: {CONFIG_PATH}") + app.run(host="0.0.0.0", port=port, debug=False) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a0d407c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +flask>=3.0 +requests>=2.31 diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..c7d6054 --- /dev/null +++ b/static/index.html @@ -0,0 +1,410 @@ + + + + + +Ollama GPU Switcher + + + + +

๐Ÿ”€ Ollama GPU Switcher

+

Toggle agents between work mode and lab experiments

+ +
+

Current Mode

+
+
+ Loading... +
+
+ + +
+
+ +
+

GPU Agents

+ +
+ +
+

Lab Agent (Eric)

+
+ loading... +
+
+ + +
+
+ +
+ + + +