From 8989815afa2df388fe8b3e5ffe2d7291f6d23a83 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 22 Mar 2026 17:47:05 +0000 Subject: [PATCH] =?UTF-8?q?Wire=20in=20GPU=20optimizations=20and=20Gitea?= =?UTF-8?q?=E2=86=94GitHub=20sync?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ollama config (all 3 setup scripts): - Models updated from Qwen 2.5 → Qwen 3.5 series (Feb 2026) - Auto-detect multi-GPU: TOTAL_VRAM = per-card × count - KV cache quantization (q8_0) + flash attention enabled by default - Context windows scaled by total VRAM (4K→128K) - RAG server CHAT_MODEL now uses detected model variable Gitea↔GitHub sync (new): - gitea-github-sync.sh: bidirectional mirror with --init wizard - Modes: --pull-only, --push-only, --list (dry run), --repo single - Auto-discovers repos from both platforms via API - Systemd timer: --install-timer [interval] for scheduled sync - MCP tool: gitea_github_sync() for on-demand from Claude/WebUI - Sync script mounted read-only into mcp-server container - .env gets GITEA_URL variable for sync script - curl added to mcp-server container deps https://claude.ai/code/session_01PtYTPherSJaxDEVPgF6Nxu --- gitea-github-sync.sh | 464 +++++++++++++++++++++++++++++++++++++++++ laptop_full_setup.sh | 114 ++++++---- local-ai-setup.sh | 44 ++-- mcp_server.py | 19 ++ ubuntu-post-install.sh | 38 ++-- 5 files changed, 609 insertions(+), 70 deletions(-) create mode 100755 gitea-github-sync.sh diff --git a/gitea-github-sync.sh b/gitea-github-sync.sh new file mode 100755 index 0000000..2624cdb --- /dev/null +++ b/gitea-github-sync.sh @@ -0,0 +1,464 @@ +#!/usr/bin/env bash +# ============================================================================= +# Gitea ↔ GitHub Mirror Sync +# +# Mirrors repos between your local Gitea and GitHub in both directions: +# GitHub → Gitea: Pulls repos you own on GitHub into Gitea (backup/offline use) +# Gitea → GitHub: Pushes Gitea repos to GitHub (remote backup) +# +# Usage: +# ./gitea-github-sync.sh — sync all configured repos +# ./gitea-github-sync.sh --pull-only — GitHub → Gitea only +# ./gitea-github-sync.sh --push-only — Gitea → GitHub only +# ./gitea-github-sync.sh --repo owner/name — sync one specific repo +# ./gitea-github-sync.sh --list — list what would sync (dry run) +# ./gitea-github-sync.sh --init — interactive first-time setup +# +# Config: ~/.config/gitea-github-sync/config +# Tokens: reads from .env in the same directory as this script (or $SYNC_ENV) +# +# Schedule: install the systemd timer with --install-timer +# ./gitea-github-sync.sh --install-timer — every 6 hours (default) +# ./gitea-github-sync.sh --install-timer 1h — custom interval +# ./gitea-github-sync.sh --remove-timer — remove the timer +# ============================================================================= +set -euo pipefail + +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' +CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m' +info() { echo -e "${CYAN}[sync]${NC} $*"; } +ok() { echo -e "${GREEN}[ ok ]${NC} $*"; } +warn() { echo -e "${YELLOW}[warn]${NC} $*"; } +err() { echo -e "${RED}[err ]${NC} $*" >&2; } + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/gitea-github-sync" +CONFIG_FILE="$CONFIG_DIR/config" +WORK_DIR="$CONFIG_DIR/repos" +LOG_FILE="$CONFIG_DIR/sync.log" + +# ── load tokens from .env ─────────────────────────────────────────────────── +ENV_FILE="${SYNC_ENV:-$SCRIPT_DIR/.env}" +if [[ -f "$ENV_FILE" ]]; then + # shellcheck disable=SC1090 + set -a; source <(grep -E '^(GITEA_TOKEN|GITHUB_TOKEN|GITEA_URL)=' "$ENV_FILE" | sed 's/ *#.*//'); set +a +fi + +GITEA_URL="${GITEA_URL:-http://localhost:3001}" +GITEA_TOKEN="${GITEA_TOKEN:-}" +GITHUB_TOKEN="${GITHUB_TOKEN:-}" + +# ── parse args ────────────────────────────────────────────────────────────── +MODE="all" # all | pull | push | list | init | install-timer | remove-timer +SINGLE_REPO="" +TIMER_INTERVAL="6h" + +while [[ $# -gt 0 ]]; do + case "$1" in + --pull-only) MODE="pull"; shift ;; + --push-only) MODE="push"; shift ;; + --list) MODE="list"; shift ;; + --init) MODE="init"; shift ;; + --install-timer) MODE="install-timer"; shift; [[ "${1:-}" =~ ^[0-9]+[smhd]$ ]] && { TIMER_INTERVAL="$1"; shift; } ;; + --remove-timer) MODE="remove-timer"; shift ;; + --repo) shift; SINGLE_REPO="${1:-}"; shift ;; + -h|--help) + sed -n '2,/^# =====/{ /^# =====/d; s/^# \?//p; }' "$0"; exit 0 ;; + *) err "Unknown arg: $1"; exit 1 ;; + esac +done + +# ── helpers ───────────────────────────────────────────────────────────────── +_gitea_api() { + local method="$1" path="$2"; shift 2 + curl -sfL -X "$method" \ + -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/json" \ + "$GITEA_URL/api/v1$path" "$@" +} + +_github_api() { + local method="$1" path="$2"; shift 2 + curl -sfL -X "$method" \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com$path" "$@" +} + +_log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"; } + +# ── config management ────────────────────────────────────────────────────── +load_config() { + mkdir -p "$CONFIG_DIR" "$WORK_DIR" + GITHUB_USER="" + GITEA_USER="" + SYNC_REPOS=() # explicit list (empty = auto-discover) + EXCLUDE_REPOS=() # repos to skip + PUSH_PRIVATE=false # push private Gitea repos to GitHub? + PULL_PRIVATE=true # pull private GitHub repos to Gitea? + PULL_FORKS=false # pull forked repos from GitHub? + + if [[ -f "$CONFIG_FILE" ]]; then + # shellcheck disable=SC1090 + source "$CONFIG_FILE" + fi +} + +save_config() { + mkdir -p "$CONFIG_DIR" + cat > "$CONFIG_FILE" << EOF +# Gitea-GitHub Sync — configuration +# Generated $(date '+%Y-%m-%d %H:%M:%S') + +# GitHub username (for discovering repos to pull) +GITHUB_USER="$GITHUB_USER" + +# Gitea username (for discovering repos to push) +GITEA_USER="$GITEA_USER" + +# Explicit repo list — if set, only these sync. Format: owner/repo +# Leave empty () to auto-discover from both platforms. +SYNC_REPOS=($(printf '"%s" ' "${SYNC_REPOS[@]}")) + +# Repos to skip (pattern matched against owner/repo) +EXCLUDE_REPOS=($(printf '"%s" ' "${EXCLUDE_REPOS[@]}")) + +# Push private Gitea repos to GitHub as private repos? +PUSH_PRIVATE=$PUSH_PRIVATE + +# Pull private GitHub repos to Gitea? +PULL_PRIVATE=$PULL_PRIVATE + +# Pull forked repos from GitHub? +PULL_FORKS=$PULL_FORKS +EOF + ok "Config saved: $CONFIG_FILE" +} + +# ── init (first-time setup) ──────────────────────────────────────────────── +do_init() { + echo -e "\n${BOLD}Gitea ↔ GitHub Sync — First-Time Setup${NC}\n" + + # Check tokens + if [[ -z "$GITEA_TOKEN" || "$GITEA_TOKEN" == "your-gitea-token-here" ]]; then + err "GITEA_TOKEN not set. Add it to $ENV_FILE first." + echo " Generate at: $GITEA_URL/user/settings/applications" + exit 1 + fi + if [[ -z "$GITHUB_TOKEN" || "$GITHUB_TOKEN" == "your-github-token-here" ]]; then + err "GITHUB_TOKEN not set. Add it to $ENV_FILE first." + echo " Generate at: https://github.com/settings/tokens" + echo " Scopes needed: repo (full control)" + exit 1 + fi + + # Discover usernames + info "Detecting GitHub user..." + GITHUB_USER=$(_github_api GET /user | python3 -c "import sys,json; print(json.load(sys.stdin)['login'])" 2>/dev/null) \ + || { err "Failed to reach GitHub API. Check GITHUB_TOKEN."; exit 1; } + ok "GitHub user: $GITHUB_USER" + + info "Detecting Gitea user..." + GITEA_USER=$(_gitea_api GET /user | python3 -c "import sys,json; print(json.load(sys.stdin)['login'])" 2>/dev/null) \ + || { err "Failed to reach Gitea API. Check GITEA_TOKEN and GITEA_URL ($GITEA_URL)."; exit 1; } + ok "Gitea user: $GITEA_USER" + + # Ask about sync scope + echo "" + read -rp "Pull private GitHub repos to Gitea? [Y/n] " ans + PULL_PRIVATE=true; [[ "${ans,,}" == "n" ]] && PULL_PRIVATE=false + + read -rp "Pull forked repos from GitHub? [y/N] " ans + PULL_FORKS=false; [[ "${ans,,}" == "y" ]] && PULL_FORKS=true + + read -rp "Push private Gitea repos to GitHub? [y/N] " ans + PUSH_PRIVATE=false; [[ "${ans,,}" == "y" ]] && PUSH_PRIVATE=true + + save_config + + echo "" + info "Run '$(basename "$0") --list' to preview what would sync." + info "Run '$(basename "$0")' to sync now." + info "Run '$(basename "$0") --install-timer' to sync automatically." +} + +# ── discover repos ───────────────────────────────────────────────────────── +get_github_repos() { + local page=1 repos=() + while true; do + local batch + batch=$(_github_api GET "/user/repos?per_page=100&page=$page&affiliation=owner" \ + | python3 -c " +import sys, json +for r in json.load(sys.stdin): + if r.get('fork') and not $($PULL_FORKS && echo True || echo False): + continue + if r.get('private') and not $($PULL_PRIVATE && echo True || echo False): + continue + print(r['full_name'] + '|' + r['clone_url'] + '|' + str(r.get('private',False)).lower()) +" 2>/dev/null) || break + [[ -z "$batch" ]] && break + while IFS= read -r line; do repos+=("$line"); done <<< "$batch" + ((page++)) + done + printf '%s\n' "${repos[@]}" +} + +get_gitea_repos() { + local page=1 repos=() + while true; do + local batch + batch=$(_gitea_api GET "/repos/search?limit=50&page=$page" \ + | python3 -c " +import sys, json +for r in json.load(sys.stdin).get('data', []): + if r.get('private') and not $($PUSH_PRIVATE && echo True || echo False): + continue + print(r['full_name'] + '|' + r['clone_url'] + '|' + str(r.get('private',False)).lower()) +" 2>/dev/null) || break + [[ -z "$batch" ]] && break + while IFS= read -r line; do repos+=("$line"); done <<< "$batch" + ((page++)) + done + printf '%s\n' "${repos[@]}" +} + +is_excluded() { + local repo="$1" + for pat in "${EXCLUDE_REPOS[@]}"; do + [[ "$repo" == $pat ]] && return 0 + done + return 1 +} + +# ── sync: GitHub → Gitea (pull) ─────────────────────────────────────────── +sync_github_to_gitea() { + local full_name="$1" clone_url="$2" is_private="$3" + local repo_name="${full_name#*/}" + local local_path="$WORK_DIR/$full_name" + + # Clone or fetch from GitHub + if [[ -d "$local_path" ]]; then + info "Fetching $full_name from GitHub..." + git -C "$local_path" fetch --all --prune --quiet 2>/dev/null || { + err "Failed to fetch $full_name"; return 1; } + else + info "Cloning $full_name from GitHub..." + mkdir -p "$(dirname "$local_path")" + local auth_url="${clone_url/https:\/\//https:\/\/$GITHUB_TOKEN@}" + git clone --bare --quiet "$auth_url" "$local_path" 2>/dev/null || { + err "Failed to clone $full_name"; return 1; } + fi + + # Ensure repo exists on Gitea + local gitea_check + gitea_check=$(_gitea_api GET "/repos/$GITEA_USER/$repo_name" 2>/dev/null) || true + if ! echo "$gitea_check" | python3 -c "import sys,json; json.load(sys.stdin)['id']" &>/dev/null; then + info "Creating $repo_name on Gitea..." + _gitea_api POST "/user/repos" \ + -d "{\"name\":\"$repo_name\",\"private\":$is_private,\"description\":\"Mirror of $full_name from GitHub\"}" \ + >/dev/null || { err "Failed to create $repo_name on Gitea"; return 1; } + fi + + # Push to Gitea + local gitea_push_url="${GITEA_URL/https:\/\//https:\/\/$GITEA_USER:$GITEA_TOKEN@}" + gitea_push_url="${gitea_push_url/http:\/\//http:\/\/$GITEA_USER:$GITEA_TOKEN@}" + gitea_push_url="$gitea_push_url/$GITEA_USER/$repo_name.git" + + git -C "$local_path" push --mirror "$gitea_push_url" --quiet 2>/dev/null || { + err "Failed to push $full_name to Gitea"; return 1; } + ok "GitHub → Gitea: $full_name" + _log "PULL $full_name OK" +} + +# ── sync: Gitea → GitHub (push) ─────────────────────────────────────────── +sync_gitea_to_github() { + local full_name="$1" clone_url="$2" is_private="$3" + local repo_name="${full_name#*/}" + local local_path="$WORK_DIR/gitea/$full_name" + + # Clone or fetch from Gitea + local gitea_auth_url="${clone_url/https:\/\//https:\/\/$GITEA_USER:$GITEA_TOKEN@}" + gitea_auth_url="${gitea_auth_url/http:\/\//http:\/\/$GITEA_USER:$GITEA_TOKEN@}" + + if [[ -d "$local_path" ]]; then + info "Fetching $full_name from Gitea..." + git -C "$local_path" fetch --all --prune --quiet 2>/dev/null || { + err "Failed to fetch $full_name from Gitea"; return 1; } + else + info "Cloning $full_name from Gitea..." + mkdir -p "$(dirname "$local_path")" + git clone --bare --quiet "$gitea_auth_url" "$local_path" 2>/dev/null || { + err "Failed to clone $full_name from Gitea"; return 1; } + fi + + # Ensure repo exists on GitHub + local gh_check + gh_check=$(_github_api GET "/repos/$GITHUB_USER/$repo_name" 2>/dev/null) || true + if ! echo "$gh_check" | python3 -c "import sys,json; json.load(sys.stdin)['id']" &>/dev/null; then + info "Creating $repo_name on GitHub..." + _github_api POST "/user/repos" \ + -d "{\"name\":\"$repo_name\",\"private\":$is_private,\"description\":\"Mirror from Gitea\"}" \ + >/dev/null || { err "Failed to create $repo_name on GitHub"; return 1; } + fi + + # Push to GitHub + local github_push_url="https://$GITHUB_TOKEN@github.com/$GITHUB_USER/$repo_name.git" + git -C "$local_path" push --mirror "$github_push_url" --quiet 2>/dev/null || { + err "Failed to push $full_name to GitHub"; return 1; } + ok "Gitea → GitHub: $full_name" + _log "PUSH $full_name OK" +} + +# ── list (dry run) ───────────────────────────────────────────────────────── +do_list() { + echo -e "\n${BOLD}Repos that would sync:${NC}\n" + + if [[ ${#SYNC_REPOS[@]} -gt 0 ]]; then + echo -e "${CYAN}Explicit list:${NC}" + printf ' %s\n' "${SYNC_REPOS[@]}" + else + if [[ "$MODE" != "push" ]]; then + echo -e "${CYAN}GitHub → Gitea (pull):${NC}" + get_github_repos | while IFS='|' read -r name url priv; do + is_excluded "$name" && echo " $name (excluded)" && continue + echo " $name $([ "$priv" = "true" ] && echo "[private]")" + done + fi + echo "" + if [[ "$MODE" != "pull" ]]; then + echo -e "${CYAN}Gitea → GitHub (push):${NC}" + get_gitea_repos | while IFS='|' read -r name url priv; do + is_excluded "$name" && echo " $name (excluded)" && continue + echo " $name $([ "$priv" = "true" ] && echo "[private]")" + done + fi + fi + echo "" +} + +# ── main sync ────────────────────────────────────────────────────────────── +do_sync() { + local pull_count=0 push_count=0 fail_count=0 + + _log "=== Sync started (mode=$MODE) ===" + + # GitHub → Gitea + if [[ "$MODE" == "all" || "$MODE" == "pull" ]]; then + info "Discovering GitHub repos..." + while IFS='|' read -r name url priv; do + [[ -z "$name" ]] && continue + [[ -n "$SINGLE_REPO" && "$name" != "$SINGLE_REPO" ]] && continue + is_excluded "$name" && continue + if sync_github_to_gitea "$name" "$url" "$priv"; then + ((pull_count++)) + else + ((fail_count++)) + fi + done < <(get_github_repos) + fi + + # Gitea → GitHub + if [[ "$MODE" == "all" || "$MODE" == "push" ]]; then + info "Discovering Gitea repos..." + while IFS='|' read -r name url priv; do + [[ -z "$name" ]] && continue + [[ -n "$SINGLE_REPO" && "${name#*/}" != "${SINGLE_REPO#*/}" ]] && continue + is_excluded "$name" && continue + # Skip repos that came from GitHub (already mirrored) + local repo_name="${name#*/}" + if [[ -d "$WORK_DIR/$GITHUB_USER/$repo_name" ]]; then + info "Skipping $name (already a GitHub mirror)" + continue + fi + if sync_gitea_to_github "$name" "$url" "$priv"; then + ((push_count++)) + else + ((fail_count++)) + fi + done < <(get_gitea_repos) + fi + + echo "" + ok "Sync complete: ${pull_count} pulled, ${push_count} pushed, ${fail_count} failed" + _log "=== Sync complete: pull=$pull_count push=$push_count fail=$fail_count ===" +} + +# ── systemd timer ────────────────────────────────────────────────────────── +install_timer() { + local service_file="/etc/systemd/system/gitea-github-sync.service" + local timer_file="/etc/systemd/system/gitea-github-sync.timer" + local script_path + script_path="$(readlink -f "$0")" + + info "Installing systemd timer (interval: $TIMER_INTERVAL)..." + + sudo tee "$service_file" > /dev/null << EOF +[Unit] +Description=Gitea-GitHub Mirror Sync +After=network-online.target docker.service +Wants=network-online.target + +[Service] +Type=oneshot +User=$USER +ExecStart=$script_path +Environment=HOME=$HOME +StandardOutput=append:$LOG_FILE +StandardError=append:$LOG_FILE +EOF + + sudo tee "$timer_file" > /dev/null << EOF +[Unit] +Description=Gitea-GitHub Sync Timer + +[Timer] +OnBootSec=5min +OnUnitActiveSec=$TIMER_INTERVAL +Persistent=true + +[Install] +WantedBy=timers.target +EOF + + sudo systemctl daemon-reload + sudo systemctl enable --now gitea-github-sync.timer + ok "Timer installed: every $TIMER_INTERVAL" + ok "Check status: systemctl status gitea-github-sync.timer" + ok "Run now: sudo systemctl start gitea-github-sync.service" + ok "Logs: $LOG_FILE" +} + +remove_timer() { + info "Removing systemd timer..." + sudo systemctl disable --now gitea-github-sync.timer 2>/dev/null || true + sudo rm -f /etc/systemd/system/gitea-github-sync.{service,timer} + sudo systemctl daemon-reload + ok "Timer removed" +} + +# ── preflight checks ────────────────────────────────────────────────────── +preflight() { + local ok=true + if [[ -z "$GITEA_TOKEN" || "$GITEA_TOKEN" == "your-gitea-token-here" ]]; then + err "GITEA_TOKEN not set. Edit $ENV_FILE"; ok=false + fi + if [[ -z "$GITHUB_TOKEN" || "$GITHUB_TOKEN" == "your-github-token-here" ]]; then + err "GITHUB_TOKEN not set. Edit $ENV_FILE"; ok=false + fi + if [[ -z "$GITEA_USER" || -z "$GITHUB_USER" ]]; then + err "Run --init first to configure usernames"; ok=false + fi + $ok || exit 1 +} + +# ── main ─────────────────────────────────────────────────────────────────── +load_config + +case "$MODE" in + init) do_init ;; + install-timer) install_timer ;; + remove-timer) remove_timer ;; + list) preflight; do_list ;; + *) preflight; do_sync ;; +esac diff --git a/laptop_full_setup.sh b/laptop_full_setup.sh index cd37f63..ba9f608 100755 --- a/laptop_full_setup.sh +++ b/laptop_full_setup.sh @@ -39,30 +39,42 @@ LOCAL_IP=$(ip route get 1.1.1.1 2>/dev/null | grep -oP 'src \K\S+' \ # Models (defaults, may be adjusted below based on VRAM) EMBED_MODEL="nomic-embed-text" -CHAT_MODEL="qwen2.5:14b" -CODE_MODEL="qwen2.5-coder:7b" -FAST_MODEL="qwen2.5:7b" +CHAT_MODEL="qwen3.5:9b" +CODE_MODEL="qwen3.5:9b" +FAST_MODEL="qwen3.5:4b" # ── detect GPU ──────────────────────────────────────────────────────────────── VRAM_GB=$(nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits 2>/dev/null \ | head -1 | awk '{printf "%d", $1/1024}' 2>/dev/null || echo "0") +GPU_COUNT=$(nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null | wc -l || echo "0") GPU_NAME=$(nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null | head -1 || echo "None") +TOTAL_VRAM=$((VRAM_GB * GPU_COUNT)) -if [[ "$VRAM_GB" -ge 14 ]]; then - CHAT_MODEL="qwen2.5:14b"; CODE_MODEL="qwen2.5-coder:14b" - GPU_TIER="16GB VRAM — 14B models" -elif [[ "$VRAM_GB" -ge 8 ]]; then - CHAT_MODEL="qwen2.5:14b"; CODE_MODEL="qwen2.5-coder:7b" - GPU_TIER="8GB VRAM — 14B chat, 7B code" -elif [[ "$VRAM_GB" -ge 4 ]]; then - CHAT_MODEL="qwen2.5:7b"; CODE_MODEL="qwen2.5-coder:7b" - GPU_TIER="6GB VRAM — 7B models" -elif [[ "$VRAM_GB" -gt 0 ]]; then - CHAT_MODEL="qwen2.5:7b"; CODE_MODEL="qwen2.5-coder:7b" - GPU_TIER="${VRAM_GB}GB VRAM — 7B models" +# Ollama optimization flags (stacked — see docs/gpu-setup-research.md) +OLLAMA_KV_CACHE="q8_0" # halves KV cache VRAM (q4_0 for aggressive) +OLLAMA_FLASH="1" # flash attention: less VRAM, no quality loss + +if [[ "$TOTAL_VRAM" -ge 40 ]]; then + CHAT_MODEL="qwen3.5:27b"; CODE_MODEL="qwen3.5:27b" + CTX=131072; GPU_TIER="${TOTAL_VRAM}GB VRAM — 27B dense, 128K context" +elif [[ "$TOTAL_VRAM" -ge 28 ]]; then + CHAT_MODEL="qwen3.5-35b-a3b"; CODE_MODEL="qwen3.5-35b-a3b" + CTX=131072; GPU_TIER="${TOTAL_VRAM}GB VRAM — 35B MoE, 128K context" +elif [[ "$TOTAL_VRAM" -ge 14 ]]; then + CHAT_MODEL="qwen3.5-35b-a3b"; CODE_MODEL="qwen3.5-35b-a3b" + CTX=65536; GPU_TIER="${TOTAL_VRAM}GB VRAM — 35B MoE + KV quant, 64K context" +elif [[ "$TOTAL_VRAM" -ge 8 ]]; then + CHAT_MODEL="qwen3.5:9b"; CODE_MODEL="qwen3.5:9b" + CTX=32768; GPU_TIER="${TOTAL_VRAM}GB VRAM — 9B dense, 32K context" +elif [[ "$TOTAL_VRAM" -ge 4 ]]; then + CHAT_MODEL="qwen3.5:4b"; CODE_MODEL="qwen3.5:4b" + CTX=16384; GPU_TIER="${TOTAL_VRAM}GB VRAM — 4B models, 16K context" +elif [[ "$TOTAL_VRAM" -gt 0 ]]; then + CHAT_MODEL="qwen3.5:4b"; CODE_MODEL="qwen3.5:4b" + CTX=8192; GPU_TIER="${TOTAL_VRAM}GB VRAM — 4B models" else - CHAT_MODEL="qwen2.5:7b"; CODE_MODEL="qwen2.5-coder:7b" - GPU_TIER="CPU only — 7B models (slow)" + CHAT_MODEL="qwen3.5:4b"; CODE_MODEL="qwen3.5:4b" + CTX=4096; OLLAMA_KV_CACHE="q4_0"; GPU_TIER="CPU only — 4B models (slow)" fi # ── new vs update ───────────────────────────────────────────────────────────── @@ -485,19 +497,19 @@ if $INSTALL_AI; then printf " %-4s %-8s %-42s %s\n" "3)" "22B" "phi4:14b + codestral:22b" "$(speed_label 13)" printf " %-4s %-8s %-42s %s\n" "4)" "70B" "llama3.3:70b + codestral:22b" "$(speed_label 41)" ;; - 2) # Performance - _TIER_NAMES=(7B 14B 32B 72B) - printf " %-4s %-8s %-42s %s\n" "1)" "7B" "qwen2.5:7b + qwen2.5-coder:7b" "$(speed_label 4)" - printf " %-4s %-8s %-42s %s\n" "2)" "14B" "qwen2.5:14b + qwen2.5-coder:14b" "$(speed_label 9)" - printf " %-4s %-8s %-42s %s\n" "3)" "32B" "qwen2.5:14b + qwen2.5-coder:32b" "$(speed_label 19)" - printf " %-4s %-8s %-42s %s\n" "4)" "72B" "qwen2.5:72b + qwen2.5-coder:32b" "$(speed_label 41)" + 2) # Performance (Qwen 3.5 — Feb 2026) + _TIER_NAMES=(4B 9B 35B 27B) + printf " %-4s %-8s %-42s %s\n" "1)" "4B" "qwen3.5:4b (chat+code)" "$(speed_label 2)" + printf " %-4s %-8s %-42s %s\n" "2)" "9B" "qwen3.5:9b (chat+code)" "$(speed_label 5)" + printf " %-4s %-8s %-42s %s\n" "3)" "35B" "qwen3.5-35b-a3b (MoE, 3B active)" "$(speed_label 12)" + printf " %-4s %-8s %-42s %s\n" "4)" "27B" "qwen3.5:27b (dense, A- quality)" "$(speed_label 17)" ;; - 3) # Mixed - _TIER_NAMES=(7B 14B 32B 70B) - printf " %-4s %-8s %-42s %s\n" "1)" "7B" "mistral:7b + qwen2.5-coder:7b" "$(speed_label 4)" - printf " %-4s %-8s %-42s %s\n" "2)" "14B" "phi4:14b + qwen2.5-coder:14b" "$(speed_label 9)" - printf " %-4s %-8s %-42s %s\n" "3)" "32B" "phi4:14b + qwen2.5-coder:32b" "$(speed_label 19)" - printf " %-4s %-8s %-42s %s\n" "4)" "70B" "llama3.3:70b + qwen2.5-coder:32b" "$(speed_label 41)" + 3) # Mixed (Western chat + Qwen 3.5 code) + _TIER_NAMES=(7B 14B 35B 70B) + printf " %-4s %-8s %-42s %s\n" "1)" "7B" "mistral:7b + qwen3.5:4b" "$(speed_label 4)" + printf " %-4s %-8s %-42s %s\n" "2)" "14B" "phi4:14b + qwen3.5:9b" "$(speed_label 9)" + printf " %-4s %-8s %-42s %s\n" "3)" "35B" "phi4:14b + qwen3.5-35b-a3b" "$(speed_label 19)" + printf " %-4s %-8s %-42s %s\n" "4)" "70B" "llama3.3:70b + qwen3.5-35b-a3b" "$(speed_label 41)" ;; esac @@ -535,16 +547,16 @@ if $INSTALL_AI; then 1:14B) FAST_MODEL="mistral:7b"; CHAT_MODEL="phi4:14b"; CODE_MODEL="starcoder2:15b"; REASON_MODEL="phi4:14b" ;; 1:22B) FAST_MODEL="mistral:7b"; CHAT_MODEL="phi4:14b"; CODE_MODEL="codestral:22b"; REASON_MODEL="phi4:14b" ;; 1:70B) FAST_MODEL="mistral:7b"; CHAT_MODEL="llama3.3:70b"; CODE_MODEL="codestral:22b"; REASON_MODEL="llama3.3:70b" ;; - # Performance-first - 2:7B) FAST_MODEL="qwen2.5:7b"; CHAT_MODEL="qwen2.5:7b"; CODE_MODEL="qwen2.5-coder:7b"; REASON_MODEL="" ;; - 2:14B) FAST_MODEL="qwen2.5:7b"; CHAT_MODEL="qwen2.5:14b"; CODE_MODEL="qwen2.5-coder:14b"; REASON_MODEL="deepseek-r1:14b" ;; - 2:32B) FAST_MODEL="qwen2.5:7b"; CHAT_MODEL="qwen2.5:14b"; CODE_MODEL="qwen2.5-coder:32b"; REASON_MODEL="deepseek-r1:14b" ;; - 2:72B) FAST_MODEL="qwen2.5:7b"; CHAT_MODEL="qwen2.5:72b"; CODE_MODEL="qwen2.5-coder:32b"; REASON_MODEL="deepseek-r1:14b" ;; - # Mixed - 3:7B) FAST_MODEL="mistral:7b"; CHAT_MODEL="mistral:7b"; CODE_MODEL="qwen2.5-coder:7b"; REASON_MODEL="" ;; - 3:14B) FAST_MODEL="mistral:7b"; CHAT_MODEL="phi4:14b"; CODE_MODEL="qwen2.5-coder:14b"; REASON_MODEL="phi4:14b" ;; - 3:32B) FAST_MODEL="mistral:7b"; CHAT_MODEL="phi4:14b"; CODE_MODEL="qwen2.5-coder:32b"; REASON_MODEL="phi4:14b" ;; - 3:70B) FAST_MODEL="mistral:7b"; CHAT_MODEL="llama3.3:70b"; CODE_MODEL="qwen2.5-coder:32b"; REASON_MODEL="llama3.3:70b" ;; + # Performance-first (Qwen 3.5) + 2:4B) FAST_MODEL="qwen3.5:4b"; CHAT_MODEL="qwen3.5:4b"; CODE_MODEL="qwen3.5:4b"; REASON_MODEL="" ;; + 2:9B) FAST_MODEL="qwen3.5:4b"; CHAT_MODEL="qwen3.5:9b"; CODE_MODEL="qwen3.5:9b"; REASON_MODEL="" ;; + 2:35B) FAST_MODEL="qwen3.5:4b"; CHAT_MODEL="qwen3.5-35b-a3b"; CODE_MODEL="qwen3.5-35b-a3b"; REASON_MODEL="" ;; + 2:27B) FAST_MODEL="qwen3.5:9b"; CHAT_MODEL="qwen3.5:27b"; CODE_MODEL="qwen3.5:27b"; REASON_MODEL="" ;; + # Mixed (Western chat + Qwen 3.5 code) + 3:7B) FAST_MODEL="mistral:7b"; CHAT_MODEL="mistral:7b"; CODE_MODEL="qwen3.5:4b"; REASON_MODEL="" ;; + 3:14B) FAST_MODEL="mistral:7b"; CHAT_MODEL="phi4:14b"; CODE_MODEL="qwen3.5:9b"; REASON_MODEL="phi4:14b" ;; + 3:35B) FAST_MODEL="mistral:7b"; CHAT_MODEL="phi4:14b"; CODE_MODEL="qwen3.5-35b-a3b"; REASON_MODEL="phi4:14b" ;; + 3:70B) FAST_MODEL="mistral:7b"; CHAT_MODEL="llama3.3:70b"; CODE_MODEL="qwen3.5-35b-a3b"; REASON_MODEL="llama3.3:70b" ;; *) warn "Unrecognised tier '$TIER_PICK' — keeping detected defaults" ;; @@ -763,8 +775,11 @@ WEBUI_URL= # Gitea — generate at http://$LOCAL_IP:3001/user/settings/applications GITEA_TOKEN=your-gitea-token-here -# GitHub — optional, for GitHub API access via MCP +# GitHub — optional, for GitHub API access via MCP and Gitea↔GitHub sync GITHUB_TOKEN=your-github-token-here + +# Gitea URL — used by sync script (default: http://localhost:3001) +GITEA_URL=http://$LOCAL_IP:3001 ENV ok "Created .env — add your tokens before using MCP Gitea/GitHub tools" else @@ -816,10 +831,12 @@ services: volumes: ${OLLAMA_VOLUME_LINE} environment: - - OLLAMA_NUM_GPU=999 # use all available VRAM (auto-detects GPU size) - - OLLAMA_NUM_CTX=8192 # lower to 4096 if you hit OOM + - OLLAMA_NUM_GPU=999 # use all available VRAM (auto-detects GPU size) + - OLLAMA_NUM_CTX=$CTX # auto-set by detected VRAM - OLLAMA_KEEP_ALIVE=24h - OLLAMA_MAX_LOADED_MODELS=1 + - OLLAMA_KV_CACHE_TYPE=$OLLAMA_KV_CACHE # q8_0 halves KV cache; q4_0 = 1/3 size + - OLLAMA_FLASH_ATTENTION=$OLLAMA_FLASH # tiled attention: less VRAM, no quality loss deploy: resources: reservations: @@ -885,7 +902,7 @@ ${OLLAMA_VOLUME_LINE} - OLLAMA_URL=http://ollama:11434 - CHROMA_URL=http://chromadb:8000 - EMBED_MODEL=nomic-embed-text - - CHAT_MODEL=qwen2.5:14b + - CHAT_MODEL=$CHAT_MODEL - PAPERS_DIR=/papers - REPOS_DIR=/repos command: > @@ -913,6 +930,7 @@ ${OLLAMA_VOLUME_LINE} - $BASE/repos:/repos - $BASE/mcp_server.py:/app/mcp_server.py - $BASE/mcp_requirements.txt:/app/mcp_requirements.txt + - $SCRIPT_DIR/gitea-github-sync.sh:/app/gitea-github-sync.sh:ro working_dir: /app env_file: $BASE/.env environment: @@ -922,7 +940,7 @@ ${OLLAMA_VOLUME_LINE} - RAG_URL=http://rag-server:8001 command: > bash -c "apt-get update -qq && - apt-get install -y --no-install-recommends git ripgrep && + apt-get install -y --no-install-recommends git ripgrep curl && pip install --no-cache-dir -r mcp_requirements.txt && python mcp_server.py" depends_on: @@ -1360,6 +1378,14 @@ if $INSTALL_AI; then echo -e " ${YELLOW}Add API tokens to:${NC} $BASE/.env" $SVC_RAG && echo -e " ${YELLOW}Drop PDFs into:${NC} $BASE/papers/" echo -e " ${YELLOW}Your workspace:${NC} $BASE/workspace/" + $SVC_GITEA && { + echo "" + echo -e " ${YELLOW}Gitea ↔ GitHub sync:${NC}" + echo " First time: $SCRIPT_DIR/gitea-github-sync.sh --init" + echo " Sync now: $SCRIPT_DIR/gitea-github-sync.sh" + echo " Auto (6h): $SCRIPT_DIR/gitea-github-sync.sh --install-timer" + echo " Via MCP: gitea_github_sync(mode='all')" + } fi if $SVC_KIWIX && [[ "$ZIM_CHOICE" == "3" ]]; then echo -e " ${YELLOW}ZIM downloads:${NC} ./kiwix_download.sh (not started)" diff --git a/local-ai-setup.sh b/local-ai-setup.sh index 2ac56bc..ab175cd 100755 --- a/local-ai-setup.sh +++ b/local-ai-setup.sh @@ -23,19 +23,31 @@ IS_UPDATE=false; [[ -f "$BASE/docker-compose.yml" ]] && IS_UPDATE=true # ── detect VRAM and set models accordingly ──────────────────────────────────── VRAM_GB=$(nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits 2>/dev/null \ | head -1 | awk '{printf "%d", $1/1024}' 2>/dev/null || echo "0") +GPU_COUNT=$(nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null | wc -l || echo "0") +TOTAL_VRAM=$((VRAM_GB * GPU_COUNT)) -if [[ "$VRAM_GB" -ge 14 ]]; then - CHAT_MODEL="qwen2.5:14b"; CODE_MODEL="qwen2.5-coder:14b" - CTX=32768; TIER="16GB — 14B models + 32k context" -elif [[ "$VRAM_GB" -ge 8 ]]; then - CHAT_MODEL="qwen2.5:14b"; CODE_MODEL="qwen2.5-coder:7b" - CTX=16384; TIER="8-16GB — 14B chat, 7B code, 16k context" -elif [[ "$VRAM_GB" -ge 4 ]]; then - CHAT_MODEL="qwen2.5:7b"; CODE_MODEL="qwen2.5-coder:7b" - CTX=8192; TIER="6GB — 7B models, 8k context" +# Ollama optimization flags (stacked — see docs/gpu-setup-research.md) +OLLAMA_KV_CACHE="q8_0" # halves KV cache VRAM (q4_0 for aggressive) +OLLAMA_FLASH="1" # flash attention: less VRAM, no quality loss + +if [[ "$TOTAL_VRAM" -ge 40 ]]; then + CHAT_MODEL="qwen3.5:27b"; CODE_MODEL="qwen3.5:27b" + CTX=131072; TIER="${TOTAL_VRAM}GB — 27B dense, 128K context" +elif [[ "$TOTAL_VRAM" -ge 28 ]]; then + CHAT_MODEL="qwen3.5-35b-a3b"; CODE_MODEL="qwen3.5-35b-a3b" + CTX=131072; TIER="${TOTAL_VRAM}GB — 35B MoE, 128K context" +elif [[ "$TOTAL_VRAM" -ge 14 ]]; then + CHAT_MODEL="qwen3.5-35b-a3b"; CODE_MODEL="qwen3.5-35b-a3b" + CTX=65536; TIER="${TOTAL_VRAM}GB — 35B MoE + KV quant, 64K context" +elif [[ "$TOTAL_VRAM" -ge 8 ]]; then + CHAT_MODEL="qwen3.5:9b"; CODE_MODEL="qwen3.5:9b" + CTX=32768; TIER="${TOTAL_VRAM}GB — 9B dense, 32K context" +elif [[ "$TOTAL_VRAM" -ge 4 ]]; then + CHAT_MODEL="qwen3.5:4b"; CODE_MODEL="qwen3.5:4b" + CTX=16384; TIER="${TOTAL_VRAM}GB — 4B models, 16K context" else - CHAT_MODEL="qwen2.5:7b"; CODE_MODEL="qwen2.5-coder:7b" - CTX=4096; TIER="CPU-only — 7B models, 4k context" + CHAT_MODEL="qwen3.5:4b"; CODE_MODEL="qwen3.5:4b" + CTX=4096; OLLAMA_KV_CACHE="q4_0"; TIER="CPU-only — 4B models, 4K context" fi EMBED_MODEL="nomic-embed-text" @@ -488,6 +500,7 @@ if [[ ! -f "$BASE/.env" ]]; then # Local AI Stack — edit to add your API tokens GITEA_TOKEN=your-gitea-token-here GITHUB_TOKEN=your-github-token-here +GITEA_URL=http://$LOCAL_IP:3001 ENV ok "Created .env" else @@ -527,9 +540,11 @@ services: volumes: [ollama-models:/root/.ollama] environment: - OLLAMA_NUM_GPU=999 - - OLLAMA_NUM_CTX= + - OLLAMA_NUM_CTX=$CTX - OLLAMA_KEEP_ALIVE=24h - OLLAMA_MAX_LOADED_MODELS=1 + - OLLAMA_KV_CACHE_TYPE=$OLLAMA_KV_CACHE + - OLLAMA_FLASH_ATTENTION=$OLLAMA_FLASH deploy: resources: reservations: @@ -586,7 +601,7 @@ services: - OLLAMA_URL=http://ollama:11434 - CHROMA_URL=http://chromadb:8000 - EMBED_MODEL=nomic-embed-text - - CHAT_MODEL= + - CHAT_MODEL=$CHAT_MODEL command: > bash -c "apt-get update -qq && apt-get install -y --no-install-recommends git && pip install --no-cache-dir -r requirements.txt && @@ -605,6 +620,7 @@ services: - $BASE/repos:/repos - $BASE/mcp_server.py:/app/mcp_server.py - $BASE/mcp_requirements.txt:/app/mcp_requirements.txt + - $SCRIPT_DIR/gitea-github-sync.sh:/app/gitea-github-sync.sh:ro working_dir: /app env_file: $BASE/.env environment: @@ -613,7 +629,7 @@ services: - GITEA_URL=http://gitea:3000 - RAG_URL=http://rag-server:8001 command: > - bash -c "apt-get update -qq && apt-get install -y --no-install-recommends git ripgrep && + bash -c "apt-get update -qq && apt-get install -y --no-install-recommends git ripgrep curl && pip install --no-cache-dir -r mcp_requirements.txt && python mcp_server.py" depends_on: [rag-server] diff --git a/mcp_server.py b/mcp_server.py index 7003ac9..5832ce6 100644 --- a/mcp_server.py +++ b/mcp_server.py @@ -180,6 +180,25 @@ def github_api(method: str, endpoint: str, body: str = "") -> str: except Exception: return r.text +# ── Gitea ↔ GitHub sync ────────────────────────────────────────────────────── +@mcp.tool() +def gitea_github_sync(mode: str = "all", repo: str = "") -> str: + """Run Gitea↔GitHub mirror sync. mode: all|pull|push|list. repo: optional owner/name.""" + cmd = ["/app/gitea-github-sync.sh"] + if mode == "pull": cmd.append("--pull-only") + elif mode == "push": cmd.append("--push-only") + elif mode == "list": cmd.append("--list") + if repo: + cmd.extend(["--repo", repo]) + try: + r = subprocess.run(cmd, capture_output=True, text=True, timeout=600, + env={**os.environ, "SYNC_ENV": "/app/.env"}) + return (r.stdout + r.stderr).strip() or "Sync completed (no output)" + except subprocess.TimeoutExpired: + return "Sync timed out after 10 minutes" + except Exception as e: + return f"Sync failed: {e}" + # ── RAG ingest ──────────────────────────────────────────────────────────────── @mcp.tool() def ingest_repo(url: str, name: str = "", branch: str = "main") -> str: diff --git a/ubuntu-post-install.sh b/ubuntu-post-install.sh index b260202..5d038e5 100644 --- a/ubuntu-post-install.sh +++ b/ubuntu-post-install.sh @@ -1106,19 +1106,31 @@ IS_UPDATE=false; [[ -f "$BASE/docker-compose.yml" ]] && IS_UPDATE=true # ── detect VRAM and set models accordingly ──────────────────────────────────── VRAM_GB=$(nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits 2>/dev/null \ | head -1 | awk '{printf "%d", $1/1024}' 2>/dev/null || echo "0") +GPU_COUNT=$(nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null | wc -l || echo "0") +TOTAL_VRAM=$((VRAM_GB * GPU_COUNT)) -if [[ "$VRAM_GB" -ge 14 ]]; then - CHAT_MODEL="qwen2.5:14b"; CODE_MODEL="qwen2.5-coder:14b" - CTX=32768; TIER="16GB — 14B models + 32k context" -elif [[ "$VRAM_GB" -ge 8 ]]; then - CHAT_MODEL="qwen2.5:14b"; CODE_MODEL="qwen2.5-coder:7b" - CTX=16384; TIER="8-16GB — 14B chat, 7B code, 16k context" -elif [[ "$VRAM_GB" -ge 4 ]]; then - CHAT_MODEL="qwen2.5:7b"; CODE_MODEL="qwen2.5-coder:7b" - CTX=8192; TIER="6GB — 7B models, 8k context" +# Ollama optimization flags (stacked — see docs/gpu-setup-research.md) +OLLAMA_KV_CACHE="q8_0" # halves KV cache VRAM (q4_0 for aggressive) +OLLAMA_FLASH="1" # flash attention: less VRAM, no quality loss + +if [[ "$TOTAL_VRAM" -ge 40 ]]; then + CHAT_MODEL="qwen3.5:27b"; CODE_MODEL="qwen3.5:27b" + CTX=131072; TIER="${TOTAL_VRAM}GB — 27B dense, 128K context" +elif [[ "$TOTAL_VRAM" -ge 28 ]]; then + CHAT_MODEL="qwen3.5-35b-a3b"; CODE_MODEL="qwen3.5-35b-a3b" + CTX=131072; TIER="${TOTAL_VRAM}GB — 35B MoE, 128K context" +elif [[ "$TOTAL_VRAM" -ge 14 ]]; then + CHAT_MODEL="qwen3.5-35b-a3b"; CODE_MODEL="qwen3.5-35b-a3b" + CTX=65536; TIER="${TOTAL_VRAM}GB — 35B MoE + KV quant, 64K context" +elif [[ "$TOTAL_VRAM" -ge 8 ]]; then + CHAT_MODEL="qwen3.5:9b"; CODE_MODEL="qwen3.5:9b" + CTX=32768; TIER="${TOTAL_VRAM}GB — 9B dense, 32K context" +elif [[ "$TOTAL_VRAM" -ge 4 ]]; then + CHAT_MODEL="qwen3.5:4b"; CODE_MODEL="qwen3.5:4b" + CTX=16384; TIER="${TOTAL_VRAM}GB — 4B models, 16K context" else - CHAT_MODEL="qwen2.5:7b"; CODE_MODEL="qwen2.5-coder:7b" - CTX=4096; TIER="CPU-only — 7B models, 4k context" + CHAT_MODEL="qwen3.5:4b"; CODE_MODEL="qwen3.5:4b" + CTX=4096; OLLAMA_KV_CACHE="q4_0"; TIER="CPU-only — 4B models, 4K context" fi EMBED_MODEL="nomic-embed-text" @@ -1611,9 +1623,11 @@ services: volumes: [ollama-models:/root/.ollama] environment: - OLLAMA_NUM_GPU=999 - - OLLAMA_NUM_CTX= + - OLLAMA_NUM_CTX=$CTX - OLLAMA_KEEP_ALIVE=24h - OLLAMA_MAX_LOADED_MODELS=1 + - OLLAMA_KV_CACHE_TYPE=$OLLAMA_KV_CACHE + - OLLAMA_FLASH_ATTENTION=$OLLAMA_FLASH deploy: resources: reservations: