Coding agents with UVA RC GenAI
2026-05-06 → 2026-07-02
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.
- Works: anything speaking OpenAI Chat Completions:
kimi-cli, Pi,aichat,llm,mods,aider. I’ve only tested Pi and it seems to work well. Pi agent is one of the simplest and most customizable agent harness. - Does not work natively: OpenAI Codex CLI (deprecated chat-completions in Feb 2026; requires the Responses API now) and Claude Code (Anthropic Messages API only). A LiteLLM proxy in front would translate Responses/Messages to chat-completions and unlock both. Untested here.
Recipe: Pi + sshuttle (macOS)#
Type pi-uva in any terminal and get a coding agent talking to UVA Kimi.
Prerequisites#
- UVA RC account with GenAI enabled, and your API key.
- UVA Anywhere VPN installed and connected (requires a personal digital certificate—works on personal computers).
- SSH access to
login.hpc.virginia.edu(NetBadge + Duo, or your SSH key uploaded to Rivanna). - Homebrew.
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
}
# Bring the tunnel up, retrying the whole launch a few times. sshuttle -D
# daemonizes and can return 0 yet never finish connecting, so each attempt
# waits for _uva_tunnel_up and tears down a dead attempt before retrying.
_uva_tunnel_start() {
local attempts=3 wait_secs=20 a=0
while ((++a <= attempts)); do
echo "→ sshuttle attempt $a/$attempts to Rivanna (sudo required)..."
sudo sshuttle -D --pidfile "$_UVA_SSHUTTLE_PID" \
-r "$_UVA_SSHUTTLE_HOST" "${_UVA_SSHUTTLE_TARGETS[@]}" || echo " ✗ sshuttle failed to launch"
local i=0
until _uva_tunnel_up; do
((++i >= wait_secs)) && break
sleep 1
done
if _uva_tunnel_up; then
echo "✓ Tunnel up"
return 0
fi
echo " ✗ not up after ${wait_secs}s — tearing down and retrying"
pi-uva-down >/dev/null 2>&1
done
echo "✗ Tunnel didn't come up after $attempts attempts"
return 1
}
pi-uva() {
if ! _uva_tunnel_up; then
sudo -v || return 1
_uva_tunnel_start || return 1
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 launch is flaky—sshuttle daemonizes and sometimes returns before the connection is actually usable—so _uva_tunnel_start retries: it waits up to 20s for the API to respond, and on failure tears the attempt down and relaunches, up to three times. 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#
- Bash users. Works with minor tweaks (replace
[[ ... ]]and the zsh array syntax). - Linux users. sshuttle uses iptables instead of pf automatically.
- Other CLIs. Any tool that takes a custom OpenAI-compatible base URL drops in.
aichat -m uva-kimionce configured,llm -m uva-kimi, etc. Same env-var trick for the key. - API-key hygiene. Do not commit your token. Put
export UVARC_GenAI_API=...in~/.env, source it from your shell rc, and gitignore~/.envglobally. - Session privacy. Pi saves every session by default as a full transcript under
~/.pi/agent/sessions/<project-path>/*.jsonl—including file contents it read and your prompts. For sensitive work that’s a local copy you may not want lingering. Add--no-sessionto thepiinvocation to run ephemerally (the launcher line becomespi --no-session --provider uva ...); note this also disables--continue/--resume. To clear what’s already saved, delete the per-project folders under~/.pi/agent/sessions/(send to Trash rather thanrmso it’s recoverable). - Different Rivanna username. The launcher assumes your local username matches your NetBadge ID. If it doesn’t, set
_UVA_SSHUTTLE_HOST="yourcomputingid@login.hpc.virginia.edu"instead of relying on$(whoami). - Auto-update Pi. Add
pi updateas the first line ofpi-uvaif you want it to self-update on every launch. - If the network gating gets relaxed later. Skip the sshuttle step—pi will reach the endpoint directly (assuming you’re still on UVA Anywhere VPN). The
models.jsonblock is unchanged.
See also#
- UVA RC GenAI user guide
- Claude Code
- Pi coding agent (
badlogic/pi-mono) - sshuttle