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.
- 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
}
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#
- 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. - 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