Coding agents with UVA RC GenAI

2026-05-06 → 2026-05-10

UVA Research Computing’s RC GenAI service exposes an OpenAI-compatible API at https://open-webui.rc.virginia.edu/api and currently serves Kimi K2.5.

The API is gated to HPC-internal traffic, so even on UVA Anywhere VPN you can’t reach it directly. The fix is to route requests through a Rivanna login node with sshuttle. Once that’s in place, your local CLI talks to RC GenAI like any other OpenAI-compatible endpoint.

Note that SSH to login.hpc.virginia.edu itself is gated to UVA networks, so you do need UVA Anywhere VPN connected on the host first—sshuttle then tunnels through that SSH session. UVA Anywhere requires a personal digital certificate, which you can obtain for a personal machine via NetBadge.

What works, what doesn’t#

Coding-agent CLIs differ in which API they speak, and that matters more than it used to. RC GenAI exposes only OpenAI Chat Completions—not the newer OpenAI Responses API and not Anthropic’s Messages API.

Recipe: Pi + sshuttle (macOS)#

Type pi-uva in any terminal and get a coding agent talking to UVA Kimi.

Prerequisites#

brew install pi-coding-agent sshuttle
export UVARC_GenAI_API="sk-..."        # put in ~/.env or shell rc; do not commit

1. Configure Pi#

Add the UVA provider to ~/.pi/agent/models.json. If the file already exists, merge the "uva" block into the existing "providers" object.

{
  "providers": {
    "uva": {
      "api": "openai-completions",
      "apiKey": "UVARC_GenAI_API",
      "baseUrl": "https://open-webui.rc.virginia.edu/api",
      "models": [
        {
          "id": "Kimi K2.5",
          "name": "Kimi K2.5 (UVA RC GenAI)",
          "reasoning": false,
          "input": ["text"],
          "contextWindow": 65536,
          "maxTokens": 8192,
          "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }
        }
      ]
    }
  }
}

The apiKey value here is the name of the env var, not the key itself—Pi looks up the value at runtime.

2. Add the launcher#

Drop this into your shell rc (~/.zshrc or wherever you keep machine-specific functions). It assumes your macOS username matches your NetBadge ID; hardcode the Rivanna user if not.

_UVA_SSHUTTLE_PID=~/.pi/uva-sshuttle.pid
_UVA_SSHUTTLE_HOST="$(whoami)@login.hpc.virginia.edu"
_UVA_SSHUTTLE_TARGETS=(open-webui.rc.virginia.edu)

_uva_tunnel_up() {
    curl -sf -o /dev/null -m 3 \
        -H "Authorization: Bearer $UVARC_GenAI_API" \
        https://open-webui.rc.virginia.edu/api/models
}

pi-uva() {
    if _uva_tunnel_up; then
        :  # tunnel already up
    else
        echo "→ Starting sshuttle to Rivanna (sudo required)..."
        sudo -v || return 1
        sudo sshuttle -D --pidfile "$_UVA_SSHUTTLE_PID" \
            -r "$_UVA_SSHUTTLE_HOST" "${_UVA_SSHUTTLE_TARGETS[@]}"
        local i=0
        until _uva_tunnel_up; do
            ((++i > 15)) && { echo "✗ Tunnel didn't come up in 15s"; return 1; }
            sleep 1
        done
        echo "✓ Tunnel up"
    fi
    pi --provider uva --model "Kimi K2.5" "$@"
}

pi-uva-down() {
    [[ -f $_UVA_SSHUTTLE_PID ]] || { echo "no tunnel"; return 0; }
    sudo kill "$(<$_UVA_SSHUTTLE_PID)" 2>/dev/null
    sudo rm -f "$_UVA_SSHUTTLE_PID"
    echo "tunnel down"
}

3. Use it#

source ~/.zshrc        # or open a new terminal
pi-uva

First run prompts for sudo (sshuttle needs it for pf rules on macOS). The tunnel persists across pi-uva invocations until you run pi-uva-down or reboot. The launcher checks tunnel health by hitting the API rather than the pidfile (the pidfile is owned by root, so kill -0 from your user fails with EPERM and would falsely report the tunnel down).

Notes#

See also#

Receive my updates

YY's Random Walks — Science, academia, and occasional rabbit holes.

YY's Bike Shed — Sustainable mobility, urbanism, and the details that matter.

×