From Local Ollama Models to a Public OpenAI-Compatible API
Running multiple AI models locally is becoming increasingly practical. With Ollama, you can run models such as Llama, Qwen, and DeepSeek on your own hardware. At the same time, cloud providers such as Anthropic, OpenAI, and Google provide access to much larger hosted models.
The problem is that every application tends to expect a slightly different API, authentication mechanism, model name, and endpoint.
A better architecture is to place a unified AI gateway in front of all of these providers.
In this project, I used OmniRoute as that gateway and exposed it through a reverse proxy using a custom HTTPS domain. The final result was an OpenAI-compatible endpoint that applications can use without needing to know whether the underlying model is running locally or in the cloud.
This article documents the complete process, including the problems encountered along the way and how they were resolved.
1. The Goal
The objective was to build something like this:
AI Client
│
▼
https://ai.example.com
│
Cloudflare
│
▼
Nginx Proxy Manager
│
▼
OmniRoute
│
┌────────────┼────────────┐
│ │ │
▼ ▼ ▼
Anthropic OpenAI Gemini
│
│
▼
Ollama
│
┌─────┼─────┐
▼ ▼ ▼
Qwen Llama DeepSeek
The important design goal was that applications would only need to know about one endpoint:
https://ai.example.com/v1
They would not need separate configurations for every provider.
2. Why Use a Gateway?
Without a gateway, every application needs its own provider configuration.
For example:
Application A → OpenAI
Application B → Anthropic
Application C → Gemini
Application D → Ollama
Application E → Another local model
This becomes difficult to maintain.
You also end up with:
- Multiple API keys
- Different API endpoints
- Different model naming conventions
- Different authentication mechanisms
- Different request formats
- Different logging and monitoring
- Different configuration for every application
With a gateway:
┌── OpenAI
│
├── Anthropic
Client → OmniRoute ─┼── Gemini
│
└── Ollama
The client only needs to know about OmniRoute.
3. Hardware and Network Architecture
The gateway was hosted on a Raspberry Pi running DietPi.
The actual addresses are intentionally anonymized in this article.
For example:
Raspberry Pi:
192.168.1.50
Ollama server:
192.168.1.100
OmniRoute:
192.168.1.50:20128
The actual production environment used different addresses.
The Raspberry Pi was already running Docker, which made it a convenient host for the gateway.
4. Existing Docker Environment
Before installing the gateway, the Raspberry Pi contained several Docker workloads.
There were multiple application containers, PostgreSQL databases, Redis instances, workers, schedulers, and buildkit containers.
The first task was therefore to clean up old workloads that were no longer required.
Containers were removed using:
docker rm -f <container-name>
Afterward, unused Docker networks were cleaned up:
docker network prune
This is an important distinction:
Removing containers does not necessarily remove their Docker networks or volumes.
Docker volumes were intentionally treated more carefully because they can contain persistent application data.
For example:
docker volume ls
should be reviewed before removing anything.
Do not blindly execute:
docker volume prune
on a server containing data you may need.
5. Portainer for Docker Management
Portainer Community Edition was also useful for managing the Docker environment.
The Portainer Agent was deployed on the Raspberry Pi:
docker run -d \
-p 9001:9001 \
--name portainer_agent \
--restart=always \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /var/lib/docker/volumes:/var/lib/docker/volumes \
portainer/agent:latest
This allowed the Docker host to be managed from a central Portainer installation.
6. Connecting Ollama
The next part was connecting the gateway to an Ollama server.
The Ollama server was running on another machine on the local network.
For example:
Ollama:
192.168.1.100:11434
Ollama exposes an HTTP API, which makes it possible for another application on the network to communicate with the local models.
The important requirement is that the OmniRoute host must be able to reach the Ollama server.
A simple connectivity test is:
curl http://192.168.1.100:11434/api/tags
If the connection is successful, Ollama should return its model list.
7. Models Available Through Ollama
The local Ollama installation contained several models.
Examples included:
qwen35-chat:latest
qwen3.5:2b
qwen35-stable:latest
qwen35-fast4k:latest
deepseek-r1-research:latest
llama3.2-chat:latest
There were also embedding models such as:
embeddinggemma
nomic-embed-text
bge-m3
One important lesson here is that an embedding model should not normally be treated as a general chat model.
For example:
llama3.2-chat
is appropriate for chat/completion workloads.
Whereas:
nomic-embed-text
bge-m3
are primarily useful for embeddings and retrieval workloads.
8. Connecting Cloud AI Providers
The gateway was also configured with cloud AI providers.
The architecture therefore became:
OmniRoute
│
┌────────────┼─────────────┐
│ │ │
▼ ▼ ▼
Anthropic OpenAI Google
│ │ │
▼ ▼ ▼
Claude GPT Gemini
The provider credentials should be stored securely.
Never hard-code API keys into scripts or publish them in documentation.
9. Creating a Public Endpoint
Once the gateway was functioning locally, the next goal was to expose it through HTTPS.
The public architecture was:
Internet
│
▼
Cloudflare
│
▼
Nginx Proxy Manager
│
▼
OmniRoute
A dedicated subdomain was used:
https://ai.example.com
The internal OmniRoute service remained on a private network address and port:
192.168.1.50:20128
This is preferable to exposing the application port directly to the Internet.
10. Nginx Proxy Manager Configuration
Nginx Proxy Manager was used as the reverse proxy.
A proxy host was created for:
ai.example.com
The forwarding configuration pointed to:
Scheme: http
Host/IP: 192.168.1.50
Port: 20128
SSL was then configured for the public hostname.
The important concept is:
HTTPS client
↓
Nginx Proxy Manager
↓
HTTP OmniRoute
The internal traffic does not need to be publicly exposed.
11. Cloudflare
The DNS record was configured through Cloudflare.
Conceptually:
ai.example.com
↓
Cloudflare
↓
Public IP
↓
Reverse Proxy
↓
OmniRoute
Cloudflare provided the public DNS and HTTPS edge.
The actual domain used in the production environment is intentionally not shown here.
12. The Authentication Problem
This was where one of the most confusing issues occurred.
Initially, a request to:
https://ai.example.com/v1
returned:
{
"error": {
"message": "Authentication required",
"type": "invalid_api_key",
"code": "invalid_api_key"
}
}
At first glance, it looked like the reverse proxy or Cloudflare configuration was dropping the authentication header.
That assumption turned out to be incorrect.
13. Testing the Public Endpoint
The first test looked similar to:
curl https://ai.example.com/v1/models \
-H 'Authorization: sk-xxxxxxxx'
This returned:
401 Unauthorized
The mistake was subtle.
The API expected an HTTP Bearer token.
The correct header format is:
Authorization: Bearer <API_KEY>
not:
Authorization: <API_KEY>
Therefore the correct request was:
curl https://ai.example.com/v1/models \
-H 'Authorization: Bearer YOUR_API_KEY'
This immediately changed the result.
14. Why This Was Confusing
The HTTP response contained headers such as:
access-control-allow-headers:
Content-Type, Authorization, x-api-key, ...
This confirmed that the service was prepared to accept authentication-related headers.
It was therefore tempting to assume that the reverse proxy was the problem.
However, testing the service directly on the Raspberry Pi provided the crucial clue.
The local endpoint was:
http://127.0.0.1:20128/v1/models
Testing authentication locally allowed the reverse proxy to be eliminated from the troubleshooting process.
This is a valuable troubleshooting technique:
Always test the application locally before troubleshooting the reverse proxy.
15. Local Testing
The correct local request was:
curl -i http://127.0.0.1:20128/v1/models \
-H 'Authorization: Bearer YOUR_API_KEY'
Once this worked, the next test was:
curl -i https://ai.example.com/v1/models \
-H 'Authorization: Bearer YOUR_API_KEY'
If both returned the model catalog, then the complete chain was confirmed.
16. The Successful Model Request
The final test wasn’t just /v1/models.
A real chat completion was performed.
For example:
curl -s https://ai.example.com/v1/chat/completions \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"model": "ollama-local/llama3.2-chat:latest",
"messages": [
{
"role": "user",
"content": "Say hello in one sentence."
}
]
}'
The response was an OpenAI-compatible chat completion:
{
"object": "chat.completion",
"model": "llama3.2-chat:latest",
"choices": [
{
"message": {
"role": "assistant",
"content": "Hello!"
}
}
]
}
This was the important milestone.
It proved that the system was not merely exposing the model catalog.
An actual inference request was successfully routed through the entire infrastructure.
17. Final Architecture
The final architecture looked like this:
INTERNET
│
▼
ai.example.com
│
▼
Cloudflare
│
▼
Nginx Proxy Manager
│
▼
OmniRoute :20128
│
┌───────────────┼────────────────┐
│ │ │
▼ ▼ ▼
Anthropic OpenAI Gemini
│ │ │
└───────────────┼────────────────┘
│
▼
Local Ollama
│
┌──────────────┼───────────────┐
▼ ▼ ▼
Qwen Llama DeepSeek
From the application’s perspective, there is now one API:
https://ai.example.com/v1
18. Using the Gateway From Applications
Applications that support OpenAI-compatible APIs can generally be configured with:
Base URL:
https://ai.example.com/v1
API Key:
YOUR_OMNIROUTE_API_KEY
Model:
ollama-local/llama3.2-chat:latest
The exact configuration depends on the application.
The key point is that the application does not need to communicate directly with Ollama.
Instead:
Application
↓
OmniRoute
↓
Selected provider/model
This creates a much cleaner architecture.
19. Why This Architecture Is Useful
There are several advantages.
One endpoint
Applications only need one base URL.
https://ai.example.com/v1
Provider abstraction
The application does not need to understand every provider’s API.
Local + cloud models
You can combine:
Local:
Qwen
Llama
DeepSeek
Cloud:
Claude
GPT
Gemini
Easier application migration
Instead of changing every application when moving from one provider to another, the gateway configuration can be changed centrally.
Centralized security
API authentication can be handled at the gateway.
Centralized routing
Model selection can be managed centrally.
20. Security Considerations
Exposing an AI gateway to the Internet requires additional security considerations.
Do not treat an OpenAI-compatible API as harmless simply because it is behind a reverse proxy.
At minimum:
- Use HTTPS.
- Require API authentication.
- Use strong, randomly generated API keys.
- Rotate keys periodically.
- Never publish API keys in screenshots.
- Never commit API keys to Git.
- Restrict access where practical.
- Keep OmniRoute and the reverse proxy updated.
- Monitor unexpected API usage.
- Avoid exposing internal administration interfaces publicly.
- Consider additional Cloudflare or reverse-proxy access controls.
One especially important lesson from this project:
API keys should never be included in screenshots, blog posts, terminal transcripts, GitHub issues, or chat messages.
If a key is accidentally exposed, revoke it and generate a replacement.
21. Troubleshooting Lessons
Several small mistakes caused unnecessary debugging time.
Mistake 1: Using Windows-specific commands on Linux
For example:
curl.exe
is typically associated with Windows environments.
On Linux/DietPi, use:
curl
Mistake 2: Using docker ls
Docker does not have:
docker ls
Use:
docker ps
For images:
docker image ls
For all containers:
docker ps -a
Mistake 3: Forgetting Bearer authentication
This:
Authorization: sk-xxxxxxxx
is not equivalent to:
Authorization: Bearer sk-xxxxxxxx
The second form is the correct authentication syntax used by the OpenAI-compatible API.
Mistake 4: Troubleshooting the proxy too early
When the public API returned 401, the first suspicion was the reverse proxy.
Testing:
127.0.0.1:20128
first proved that the application itself was working.
This dramatically reduced the troubleshooting scope.
22. A Better Troubleshooting Method
When deploying a similar architecture, troubleshoot from the inside out.
Step 1 — Test Ollama
curl http://OLLAMA_HOST:11434/api/tags
Step 2 — Test OmniRoute locally
curl http://127.0.0.1:20128/v1/models \
-H 'Authorization: Bearer YOUR_API_KEY'
Step 3 — Test local completion
POST /v1/chat/completions
Step 4 — Test Nginx internally
Verify that the reverse proxy can reach:
OMNIROUTE_HOST:20128
Step 5 — Test the public hostname
curl https://ai.example.com/v1/models \
-H 'Authorization: Bearer YOUR_API_KEY'
Step 6 — Test a real completion
Only after /v1/models works should you test:
/v1/chat/completions
This layered approach makes it much easier to determine whether the problem is:
Ollama
↓
OmniRoute
↓
Authentication
↓
Reverse Proxy
↓
Cloudflare
↓
Internet
23. What I Would Improve Next
Now that the basic architecture is working, there are several improvements worth considering.
Model routing
Create logical routes for different workloads:
fast
coding
reasoning
local
cheap
premium
For example:
fast
└── local Qwen/Llama
reasoning
├── DeepSeek
└── Claude
coding
├── Claude
├── GPT
└── Qwen
local
├── Qwen
├── Llama
└── DeepSeek
This allows applications to request a capability rather than caring about the underlying infrastructure.
24. Example Future Architecture
The system can eventually evolve into:
AI CLIENTS
│
┌────────────────────┼────────────────────┐
│ │ │
OpenClaw IDE/CLI Web Apps
│ │ │
└────────────────────┼────────────────────┘
│
▼
ai.example.com
│
Cloudflare
│
Nginx Proxy Manager
│
OmniRoute
│
┌────────────────────┼────────────────────┐
│ │ │
▼ ▼ ▼
Claude GPT Gemini
│
└────────────────────┬────────────────────┘
│
Ollama
│
┌───────────────┼────────────────┐
▼ ▼ ▼
Qwen DeepSeek Llama
At this point, OmniRoute becomes the central control plane for AI workloads.
25. Conclusion
The most important outcome of this project was not simply getting Ollama to respond.
The goal was to build a reusable AI infrastructure layer.
The final system provides:
- A single OpenAI-compatible API
- HTTPS access
- Reverse-proxy integration
- Cloud AI providers
- Local Ollama models
- Centralized authentication
- A consistent API interface
- The ability to change models without reconfiguring every client
The final endpoint looks simple:
https://ai.example.com/v1
Behind that single endpoint, however, is a complete AI routing layer.
The biggest troubleshooting lesson was equally simple:
Test each layer independently before debugging the next layer.
Start with Ollama.
Then OmniRoute.
Then authentication.
Then the reverse proxy.
Then Cloudflare.
Then the public endpoint.
That approach turns what initially looks like a complicated AI infrastructure problem into a series of small, testable components.
And once everything is working, the complexity disappears from the applications consuming the API.
They simply connect to one endpoint and let the gateway handle the rest.