PewDiePie's )
Ollama serving a model (the Cookbook makes this easy)
Step 1: Verify your local model is running
After setting up Odysseus and downloading a model through Cookbook, confirm Ollama is serving:
curl http://localhost:11434/v1/models
You should see your model listed. Test a completion:
curl http://localhost:11434/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "qwen2.5:14b",
"messages": [{"role": "user", "content": "Hello"}],
"max_tokens": 50
}'
If you're using vLLM instead of Ollama, it's on port 8000 by default:
curl http://localhost:8000/v1/models
For llama.cpp server, default port is 8080:
curl http://localhost:8080/v1/models
All three speak the same OpenAI-compatible format. The rest of this guide uses Ollama on port 11434, but substitute your port if different.
Step 2: Set up an authenticated reverse proxy
This is critical. Your local model server has zero authentication by default. Before exposing it through any tunnel, you need a proxy that enforces a Bearer token.
Option A: nginx (recommended for production)
Install nginx if not already present:
# Ubuntu/Debian
sudo apt install nginx
# macOS
brew install nginx
Create the proxy config:
sudo tee /etc/nginx/sites-available/llm-proxy << 'EOF'
server {
listen 11435;
location / {
# Enforce Bearer token authentication
set \$expected_token "sk-local-YOUR-SECRET-TOKEN-HERE";
if (\$http_authorization != "Bearer \$expected_token") {
return 401 '{"error": "unauthorized"}';
}
# Proxy to local Ollama
proxy_pass http://127.0.0.1:11434;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_read_timeout 300s; # LLM inference can be slow
proxy_send_timeout 300s;
# Streaming support (important for chat completions)
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding on;
}
}
EOF
sudo ln -sf /etc/nginx/sites-available/llm-proxy /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Generate a strong token:
# Generate a random token
openssl rand -hex 32
# Output: a1b2c3d4e5f6... (use this as your token)
Test the authenticated endpoint:
# Should fail (no token)
curl http://localhost:11435/v1/models
# → 401 unauthorized
# Should succeed (with token)
curl http://localhost:11435/v1/models \
-H "Authorization: Bearer sk-local-YOUR-SECRET-TOKEN-HERE"
# → {"object":"list","data":[{"id":"qwen2.5:14b",...}]}
Option B: Caddy (simpler config)
# Caddyfile
:11435 {
@auth {
header Authorization "Bearer sk-local-YOUR-SECRET-TOKEN-HERE"
}
handle @auth {
reverse_proxy localhost:11434
}
respond 401
}
Option C: litellm proxy (if you want model aliasing)
:
tailscale cert your-machine.tailnet-name.ts.net
Option C: SSH Reverse Tunnel (quick and dirty)
If you have a VPS or any server with a public IP:
# From your local machine, tunnel port 11435 to the remote server's port 9000
ssh -R 9000:localhost:11435 [email protected] -N
# The cloud agent connects to:
# http://your-vps.com:9000/v1/chat/completions
Make it persistent with autossh:
autossh -M 0 -f -R 9000:localhost:11435 [email protected] -N \
-o "ServerAliveInterval 30" -o "ServerAliveCountMax 3"
Option D: NAT Port Forward (classic, no dependencies)
On your router:
- Forward external port 11435 → internal IP:11435
- Set up Dynamic DNS (e.g., noip.com, DuckDNS) if you don't have a static IP
Add TLS with Let's Encrypt + certbot on your nginx:
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d llm.yourdomain.com
Updated nginx config becomes:
server {
listen 443 ssl;
server_name llm.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/llm.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/llm.yourdomain.com/privkey.pem;
location / {
set \$expected_token "sk-local-YOUR-SECRET-TOKEN-HERE";
if (\$http_authorization != "Bearer \$expected_token") {
return 401 '{"error": "unauthorized"}';
}
proxy_pass http://127.0.0.1:11434;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
proxy_buffering off;
}
}
Step 4: Point ClawBase at your tunnel
This is the only change on the ClawBase side. Open your agent, go to the Model tab, and:
- Under AI Source, select "Use your own API key"
- Set Provider to "Custom (OpenAI-compatible)"
- Fill in the three fields that appear:
Base URL: https://your-tunnel-url.com/v1
Model: qwen2.5:14b (or whatever you're serving)
API Key: sk-local-YOUR-SECRET-TOKEN-HERE
- Click Save Settings
That's it. The "Custom (OpenAI-compatible)" provider accepts any endpoint that speaks the standard /v1/chat/completions format — Ollama, vLLM, llama.cpp, or anything behind your tunnel.
Verify it works before saving:
curl https://your-tunnel-url.com/v1/chat/completions \
-H "Authorization: Bearer sk-local-YOUR-SECRET-TOKEN-HERE" \
-H "Content-Type: application/json" \
-d '{
"model": "qwen2.5:14b",
"messages": [{"role": "user", "content": "What is 2+2?"}],
"max_tokens": 50
}'
If you get a response from your local model, the tunnel is working.
Step 5: Verify the hybrid setup
At this point you have two parallel paths to the same local model:
| Interface | Path | Memory | Background tasks |
|---|---|---|---|
Odysseus (local UI) | Direct to Ollama on localhost | ChromaDB (basic vector) | Only while app is open |
ClawBase (cloud agent) | Through tunnel to Ollama | 6-layer compound stack | Cron, scheduled, 24/7 |
| Telegram/Slack | Through ClawBase → tunnel → Ollama | 6-layer compound stack | Anytime, anywhere |
Both use your GPU for inference. Neither pays OpenAI or Anthropic a cent.
Test the memory:
- Tell ClawBase something: "My main project uses Next.js with Supabase. I prefer terse responses."
- Close the conversation.
- Open a new conversation hours later: "What stack is my project using?"
- The agent remembers.
Try the same in Odysseus. Depending on the model and ChromaDB config, it may or may not retain this. The 6-layer stack (journal, DAG, QMD, Mem0, Cognee, Graphiti) is what makes the difference — each layer captures context differently, so things don't just get stuffed into a vector store and forgotten.
Step 6: Systemd service (keep it running)
Make the authenticated proxy and tunnel start on boot:
# /etc/systemd/system/llm-tunnel.service
[Unit]
Description=LLM Tunnel (Cloudflare)
After=network-online.target ollama.service
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/local/bin/cloudflared tunnel run llm-tunnel
Restart=always
RestartSec=10
User=your-username
[Install]
WantedBy=multi-user.target
sudo systemctl enable --now llm-tunnel
For the SSH tunnel variant:
# /etc/systemd/system/llm-ssh-tunnel.service
[Unit]
Description=LLM SSH Reverse Tunnel
After=network-online.target
[Service]
Type=simple
ExecStart=/usr/bin/ssh -R 9000:localhost:11435 [email protected] -N -o "ServerAliveInterval 30" -o "ServerAliveCountMax 3" -o "ExitOnForwardFailure yes"
Restart=always
RestartSec=15
User=your-username
[Install]
WantedBy=multi-user.target
Security considerations
You're exposing a local service to the internet. Take this seriously:
Always use the auth proxy. Never tunnel raw Ollama/vLLM without authentication.
Rotate your token periodically. Store it as an environment variable, not hardcoded.
Use TLS. Cloudflare Tunnel handles this automatically. For NAT port forward, use Let's Encrypt.
Rate limit. Add rate limiting in nginx to prevent abuse if your token leaks:
limit_req_zone $binary_remote_addr zone=llm:10m rate=10r/m;
location / {
limit_req zone=llm burst=5;
# ... rest of proxy config
}
Monitor logs. Check nginx access logs for unexpected requests:
tail -f /var/log/nginx/access.log | grep 11435
IP allowlist. If your cloud agent has a static IP, lock it down:
allow 1.2.3.4; # ClawBase IP
deny all;
Performance notes
Local model inference over a tunnel adds network latency. Expect:
| Setup | Time to first token |
|---|---|
| Odysseus → Ollama (localhost) | ~50-200ms |
| ClawBase → Tunnel → Ollama | ~200-500ms (depending on tunnel) |
| ClawBase → OpenAI API | ~300-800ms |
The tunnel adds latency comparable to a normal API call. For most use cases (agent tasks, background work, Telegram messages), this is imperceptible. For real-time streaming chat, you'll feel it — use Odysseus locally for that.
Throughput depends on your GPU and model size. A 14B model on an RTX 4090 generates ~50 tokens/sec. Through a tunnel, the bottleneck is always inference speed, not the network.
What's next
This works today with no code changes to either project. A couple of things I'm watching:
Odysseus API — Odysseus is 4 days old. If it exposes an API for external access or webhooks for incoming messages, the integration gets tighter: conversations stored in both places, memory synced both ways.
MCP bridge — Both Odysseus and OpenClaw support MCP. A shared MCP server for memory could let both frontends read and write to the same knowledge base.
You don't have to pick sides. Your model stays local, your inference stays free, and the memory layer lives wherever makes sense for your setup.
If you want to try this setup, has a 7-day free trial starting at $16/mo. The tunnel takes about 10 minutes.
Questions? Drop a comment or find me on Twitter/X.
SOCIAL SHARE CARD GENERATOR