A local HTTPS-to-HTTP proxy that lets you use local or network LLM backends with Agile-V Studio without running into mixed-content browser restrictions.
Browser (HTTPS) ---> Proxy (HTTPS:8443) ---> LLM Backend (HTTP)
| | |
studio.agile-v.org 192.168.x.x:8443 e.g. Ollama :11434
or LLM Proxy :3015
Agile-V Studio runs over HTTPS. When you configure a local LLM (like Ollama) or a network LLM proxy as your backend, the browser blocks the request because it's HTTP β a mixed-content violation that can't be bypassed with CSP headers.
This proxy sits on your local network, terminates TLS with a self-signed certificate, and forwards requests to your HTTP backend. The browser sees an HTTPS connection and allows it.
- Zero dependencies β runs on Node.js built-ins only (
https,http,fs) - Works with any LLM backend β Ollama, llama.cpp, LocalAI, LM Studio, vLLM, LiteLLM, or any OpenAI/Anthropic-compatible API
- Token tracking β built-in dashboard and JSON API for monitoring usage
- CORS handling β supports both OpenAI and Anthropic request headers
- Streaming β pipes responses through without buffering (SSE/chunked)
- Persistent stats β token counts survive restarts
cp config.example.json config.jsonEdit config.json to point at your LLM backend:
{
"proxy": {
"port": 8443,
"allowedOrigins": "*"
},
"target": {
"host": "127.0.0.1",
"port": 11434
}
}See Configuration for all options.
./generate-cert.shNode.js:
node proxy.jsDocker:
docker compose up -d# OpenAI-compatible backend (e.g. Ollama)
curl -sk https://localhost:8443/api/tags
# Anthropic-compatible backend
curl -sk https://localhost:8443/v1/messages \
-H "Content-Type: application/json" \
-H "anthropic-version: 2023-06-01" \
-H "x-api-key: dummy" \
-d '{"model":"claude-haiku-4-5-20251001","max_tokens":20,"messages":[{"role":"user","content":"Hi"}]}'The certificate is self-signed. For browser fetch() requests to work
(which is how Agile-V Studio's browser proxy calls your LLM), the
certificate must be trusted at the system or browser level.
Simply clicking through a browser warning is not sufficient β
fetch() will still reject untrusted certificates.
sudo security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain certs/cert.pemWorks for Chrome and Safari. Firefox uses its own certificate store (see below).
Firefox ignores the system keychain. Two options:
Option 1: Open about:config in Firefox and set
security.enterprise_roots.enabled to true. Firefox will then use
the system keychain.
Option 2: Manually import under Settings > Privacy & Security >
Certificates > View Certificates > Import > select certs/cert.pem >
check "Trust this CA".
sudo cp certs/cert.pem /usr/local/share/ca-certificates/llm-proxy.crt
sudo update-ca-certificatescertutil -addstore "Root" certs\cert.pemThe proxy automatically extracts token usage from LLM responses. Works with both API formats:
- OpenAI:
prompt_tokens,completion_tokens,total_tokens - Anthropic:
input_tokens,output_tokens,cache_creation_input_tokens,cache_read_input_tokens
Open in your browser:
https://<proxy-ip>:8443/_proxy/dashboard
Shows total token usage, breakdown by model and endpoint, and recent requests. Auto-refreshes every 5 seconds.
# Get token stats as JSON
curl -sk https://localhost:8443/_proxy/stats
# Reset all stats
curl -sk -X POST https://localhost:8443/_proxy/resetExample response:
{
"uptime": "2h 15m",
"requests": 42,
"completions": 38,
"errors": 0,
"tokens": {
"prompt": 12500,
"completion": 8300,
"total": 20800,
"formatted": "20.8k"
},
"byModel": {
"claude-haiku-4-5-20251001": { "requests": 20, "total": 10400 },
"qwen3:8b-q4_K_M": { "requests": 18, "total": 10400 }
}
}Stats are persisted to stats.json every 30 seconds and on shutdown.
The proxy is configured via config.json. Copy the example to get started:
cp config.example.json config.json{
"proxy": {
"port": 8443,
"allowedOrigins": "*"
},
"target": {
"host": "127.0.0.1",
"port": 11434
},
"tls": {
"cert": "./certs/cert.pem",
"key": "./certs/key.pem"
},
"tracking": {
"enabled": true,
"statsFile": "./stats.json",
"persistInterval": 30,
"historySize": 100
}
}| Section | Key | Default | Description |
|---|---|---|---|
proxy |
port |
8443 |
HTTPS port the proxy listens on |
allowedOrigins |
"*" |
Allowed CORS origins (comma-separated or *) |
|
target |
host |
"127.0.0.1" |
Target LLM backend host |
port |
11434 |
Target LLM backend port (Ollama default) | |
tls |
cert |
"./certs/cert.pem" |
Path to TLS certificate |
key |
"./certs/key.pem" |
Path to TLS private key | |
tracking |
enabled |
true |
Enable/disable token tracking |
statsFile |
"./stats.json" |
Where to persist token stats | |
persistInterval |
30 |
Save stats to disk every N seconds | |
historySize |
100 |
Number of recent requests to keep in memory |
Local Ollama (default, no changes needed):
{
"target": { "host": "127.0.0.1", "port": 11434 }
}Network LLM proxy (e.g. LiteLLM, custom API proxy):
{
"target": { "host": "192.168.178.90", "port": 3015 }
}Ollama on another machine:
{
"target": { "host": "192.168.178.42", "port": 11434 }
}Restrict CORS to your Agile-V Studio instance:
{
"proxy": { "allowedOrigins": "https://studio.agile-v.org,https://localhost:3000" }
}Disable token tracking:
{
"tracking": { "enabled": false }
}node proxy.js --config /path/to/my-config.jsonEnvironment variables take precedence over config.json:
TARGET_HOST=10.0.0.5 TARGET_PORT=8080 node proxy.js| Env Variable | Overrides |
|---|---|
PROXY_PORT |
proxy.port |
TARGET_HOST |
target.host |
TARGET_PORT |
target.port |
CERT_FILE |
tls.cert |
KEY_FILE |
tls.key |
ALLOWED_ORIGINS |
proxy.allowedOrigins |
Run the proxy on a machine that can reach your LLM backend over HTTP. This can be the same machine (for Ollama) or any machine on your network.
See "Trust the Certificate" above. Without this step, the browser will
reject fetch() requests to the proxy.
In Agile-V Studio, go to:
- Project Settings > LLM Configuration, or
- User Settings > LLM Configuration
Select "Custom Local LLM" as the provider and set the endpoint to:
https://<proxy-ip>:8443/v1
For example:
https://192.168.178.56:8443/v1
Important: The endpoint should end with /v1. The proxy forwards
the full path, so the browser calls
https://192.168.178.56:8443/v1/messages and the proxy forwards to
http://<target>/v1/messages.
The browser connects to the proxy over HTTPS, the proxy forwards to
your LLM backend over HTTP. No more mixed-content errors. Monitor
token usage at /_proxy/dashboard.
proxy.js # Proxy server (Node.js, zero deps)
config.example.json # Example configuration (copy to config.json)
config.json # Your configuration (not in git)
generate-cert.sh # Certificate generator (OpenSSL)
Dockerfile # Docker image (node:20-alpine)
docker-compose.yml # One-command Docker start
stats.json # Persisted token stats (auto-generated, not in git)
certs/ # Generated certificates (not in git)
cert.pem # TLS certificate
key.pem # Private key
combined.pem # Both in one file
- The proxy starts an HTTPS server with the self-signed certificate
- Requests to
/_proxy/*are handled internally (stats, dashboard, reset) - All other requests are forwarded 1:1 to the HTTP target
- CORS headers are set automatically, supporting both OpenAI and
Anthropic-specific headers (
anthropic-version,x-api-key, etc.) - For completion endpoints (
/chat/completions,/v1/messages,/api/generate,/api/chat), the response is parsed to extract token usage - Response bodies are streamed (piped), not buffered β works for long LLM responses and SSE streams
- Connection errors to the target return a clean JSON 502 response
The LLM backend is not running or not reachable:
# Test the target directly
curl http://<target-host>:<target-port>/Firefox uses its own certificate store. See "Trust the Certificate > Firefox" above.
Two possible causes:
-
Certificate not trusted: The browser rejects the TLS connection before CORS is even checked. Solution: trust the certificate at the system level.
-
Header not allowed: If your LLM client sends additional headers not in
Access-Control-Allow-Headers. The proxy already allows:Content-Type,Authorization,x-api-key,anthropic-version, and variousx-stainless-*headers.
The endpoint in Agile-V Studio still points directly to http://...
instead of the proxy (https://...). Change the endpoint to
https://<proxy-ip>:8443/v1.
Inside a Docker container, localhost refers to the container itself.
The docker-compose.yml sets TARGET_HOST=host.docker.internal which
resolves to the host machine on Docker Desktop (macOS/Windows).
On Linux, use network_mode: host:
services:
llm-proxy:
build: .
network_mode: host
environment:
- TARGET_HOST=127.0.0.1./generate-cert.shThis overwrites the existing files. Restart the proxy and update the trust in your keychain or browser afterward.
MIT