ONLYOFFICE AI → FreeLLMAPI via Caddy CORS Proxy
System: Omarchy (Arch Linux) · ONLYOFFICE Desktop Editors (AUR, native) · FreeLLMAPI on 127.0.0.1:3001
Status:
Working. Caddy runs as a systemd service.
1. Summary
| Item | Value |
|---|---|
| Symptom | ONLYOFFICE AI Settings → AI Models showed “Connection error”, and the Model dropdown was empty |
| Root cause | CORS. FreeLLMAPI sends no Access-Control-Allow-* headers. It also sends Helmet-style headers (Cross-Origin-Resource-Policy: same-origin, CSP, COOP). The AI plugin sends its requests from inside the editor’s embedded browser, which enforces CORS and blocks the response |
| Fix | A Caddy reverse proxy on http://127.0.0.1:3002 (localhost only) that adds CORS headers and removes the blocking ones |
| ONLYOFFICE Base URL | http://127.0.0.1:3002/v1 |
| Other apps | Unchanged. They keep using http://localhost:3001/v1 directly |
2. Install Caddy
sudo pacman -S caddy
caddy version
3. Configure Caddy
Working copy at ~/Caddyfile:
cat > ~/Caddyfile <<'EOF'
http://127.0.0.1:3002 {
bind 127.0.0.1
log {
output stderr
format console
}
header {
Access-Control-Allow-Origin "*"
Access-Control-Allow-Headers "*"
Access-Control-Allow-Methods "GET, POST, OPTIONS"
-Cross-Origin-Resource-Policy
-Cross-Origin-Opener-Policy
-Content-Security-Policy
}
@preflight method OPTIONS
handle @preflight {
respond 204
}
reverse_proxy 127.0.0.1:3001
}
EOF
What each part does:
http://127.0.0.1:3002: the site address. Thehttp://prefix is required: without it, Caddy sees an IP address, turns on automatic HTTPS with its local CA (log lines: “enabling automatic TLS”,pki.ca.localerrors), and plainhttp://requests fail.bind 127.0.0.1: makes Caddy actually listen only on localhost, so other devices on the LAN can’t reach it (important with*CORS). The hostname in the site address only decides which requests the site answers. Withoutbind, Caddy still listens on all interfaces ("addr":":3002"in the log,*:3002inss).header { ... }: adds the CORS headers and removes FreeLLMAPI’s cross-origin-blocking headers.@preflight→respond 204: answers browser preflightOPTIONSrequests directly.reverse_proxy: forwards everything else to FreeLLMAPI.log: shows each request (method, path, status). Useful for debugging; you can remove it later.
Test in the foreground (optional)
caddy run --config ~/Caddyfile # Ctrl+C to stop
4. Run as a system service
sudo cp ~/Caddyfile /etc/caddy/Caddyfile
sudo systemctl enable --now caddy
systemctl status caddy --no-pager
After editing the Caddyfile later:
sudo cp ~/Caddyfile /etc/caddy/Caddyfile
sudo systemctl restart caddy
Check it’s bound to localhost only:
ss -tlnp | grep 3002 # should show 127.0.0.1:3002, not *:3002
5. ONLYOFFICE AI Settings
AI Settings → AI Models → Connect new AI Model
| Field | Value |
|---|---|
| Provider | OpenAI Compatible |
| API key | FreeLLMAPI unified key (freellmapi-…, from the FreeLLMAPI Keys page) |
| Base URL | http://127.0.0.1:3002/v1 |
| Model | auto |
| Profile name | freellm |
Then open the Model Assignment tab and assign freellm to each task (Chat, Summarization, Translation, Text analysis…). Tasks without a model stay greyed out.
“Import Custom Provider” is not needed. It only loads a JS adapter for APIs that don’t use the OpenAI format, and it runs in the same browser, so it hits the same CORS block.
6. Debug process (how the issue was found)
Step 1: Is the server running, and is the key valid?
curl -s http://127.0.0.1:3001/v1/models -H "Authorization: Bearer <unified-key>"
Returned JSON with the model list (auto,fusion, …) → server, port and key are all fine.
401 → wrong key. Use the unified key, not a provider key.
Connection refused → FreeLLMAPI isn’t running, or it’s on a different port.- Conclusion: the problem is between the editor and the server.
Step 2: Rule out localhost vs IPv6
- Changed the Base URL from
localhostto127.0.0.1. The editor is built on Chromium, which may try IPv6::1first.
Still “Connection error” → not an address-resolution problem.- Docker host networking wasn’t a factor: ONLYOFFICE is installed natively from the AUR.
Step 3: Check CORS (preflight)
curl -si -X OPTIONS http://127.0.0.1:3001/v1/models \
-H "Origin: null" \
-H "Access-Control-Request-Method: GET" \
-H "Access-Control-Request-Headers: authorization" | grep -i access-control
Empty output → no CORS headers.
Full response without grep:
curl -i -X OPTIONS http://127.0.0.1:3001/v1/models \
-H "Origin: onlyoffice://plugin" \
-H "Access-Control-Request-Method: GET" \
-H "Access-Control-Request-Headers: authorization,content-type"
Findings:
HTTP/1.1 200 OK+Allow: GET, HEAD→ a generic OPTIONS reply, not CORS handling.- No
Access-Control-Allow-Origin→ the browser drops the response. Cross-Origin-Resource-Policy: same-origin, CSP, COOP (Helmet defaults) → also block cross-origin use.- Root cause: CORS. curl works because it doesn’t enforce CORS. The editor’s embedded browser does.
Step 4: Rule out “Import Custom Provider”
- Same browser, same CORS block → not a fix.
Step 5: Add Caddy and check the headers
curl -si -X OPTIONS http://127.0.0.1:3002/v1/models \
-H "Origin: onlyoffice://plugin" -H "Access-Control-Request-Method: GET" | grep -i access-control
Shows Access-Control-Allow-Origin: *and the otherAllow-*headers.
Step 6: Watch the editor’s real requests (if it still fails)
Run caddy run --config ~/Caddyfile in the foreground (with the log block), then retest in ONLYOFFICE:
- No log lines → the editor isn’t calling the proxy (check the Base URL/port, and that Caddy is running).
/v1/v1/models404 → the client adds/v1itself; drop/v1from the Base URL.- 401 → key problem.
After the Caddyfile was rewritten cleanly and Caddy restarted, the dropdown filled in → fixed.
Step 7: Harden the setup and make it permanent
- Site address
http://127.0.0.1:3002+bind 127.0.0.1, checked withss -tlnp | grep 3002. - Pitfall: writing
127.0.0.1:3002withouthttp://turns on automatic HTTPS and breaks the plain-HTTP Base URL. Check withjournalctl -u caddy -n 20 --no-pager; there should be no TLS orpkilines.HTTP/2 skipped because it requires TLSandHTTP/3 skipped…warnings are normal for plain HTTP. - Installed as a systemd service (section 4).
Quick decision tree
- curl
/v1/modelsfails → fix the server or key. - curl works, but the editor fails → check the CORS preflight headers.
- No
Access-Control-Allow-Origin→ put a CORS proxy (Caddy) in front. - Proxy has the headers but the editor still fails → watch the Caddy log for the path and status code.
7. Maintenance
systemctl status caddy --no-pager # service state
journalctl -u caddy -f # live logs
sudo systemctl restart caddy # after config changes
sudo systemctl disable --now caddy # turn off
- If FreeLLMAPI ever adds a CORS option (e.g.
CORS_ORIGIN/ALLOWED_ORIGINSin its.env), you can drop Caddy and point ONLYOFFICE back to:3001. - If you regenerate the FreeLLMAPI unified key, update it in ONLYOFFICE AI Settings.