# Mirrowel's LLM gateway asks you to rename every model as provider/model_name, and mounts four host directories

> A self-hosted proxy that puts an OpenAI-shaped and an Anthropic-shaped endpoint in front of several providers, with key rotation, failover and cooldowns in a companion library. What the docs do not line up is the model naming, the dependency file, and the release list.

**Mirrowel/LLM-API-Key-Proxy** — Universal LLM Gateway: One API, every LLM. OpenAI/Anthropic-compatible endpoints with multi-provider translation and intelligent load-balancing.

- Repository: https://github.com/Mirrowel/LLM-API-Key-Proxy
- Stars: 556 · Forks: 107
- Language: Python
- License: NOASSERTION
- Published: 2026-09-10 · Updated: 2026-09-10 · Language: en
- Canonical page: https://hysenlabs.com/projects/mirrowel-llm-api-key-proxy

## Zero code changes holds as long as you can retype the model name

The pitch is one proxy, any LLM provider, zero code changes, and the mechanics are two components: a FastAPI application serving /v1/chat/completions in OpenAI shape and /v1/messages in Anthropic shape, plus a reusable library for API key management, rotation and failover, installed from inside the repository. The catch sits in the client side of the table. Every model has to be written as provider/model_name, so a request for gpt-4o becomes gemini/gemini-2.5-flash, and an application that hardcodes its model string breaks. The clearest case is the Claude Code setup, where three environment variables map Opus, Sonnet and Haiku onto non-Anthropic models:

```json
{
  "env": {
    "ANTHROPIC_AUTH_TOKEN": "your-proxy-api-key",
    "ANTHROPIC_BASE_URL": "http://127.0.0.1:8000",
    "ANTHROPIC_DEFAULT_OPUS_MODEL": "gemini/gemini-3-pro",
    "ANTHROPIC_DEFAULT_SONNET_MODEL": "gemini/gemini-3-flash",
    "ANTHROPIC_DEFAULT_HAIKU_MODEL": "openai/gpt-5-mini"
  }
}
```

## The model table and the Claude Code block name different models

The routing table names gemini/gemini-2.5-flash, openai/gpt-4o, anthropic/claude-3-5-sonnet, openrouter/anthropic/claude-3-opus and gemini_cli/gemini-2.5-pro.

```
gemini/gemini-2.5-flash          ← Gemini API
openai/gpt-4o                    ← OpenAI API
anthropic/claude-3-5-sonnet      ← Anthropic API
openrouter/anthropic/claude-3-opus  ← OpenRouter
gemini_cli/gemini-2.5-pro        ← Gemini CLI (OAuth)
```

The Claude Code block names gemini/gemini-3-pro, gemini/gemini-3-flash and openai/gpt-5-mini, none of which appear in that table. The two lists also disagree about slashes: the rule is one, and the OpenRouter row carries two. Both examples survive a copy, and the routing decision for openrouter/anthropic/claude-3-opus has to come from the code rather than from the sentence that defines the format. The five client recipes underneath inherit the same rule, whether the sample is the OpenAI SDK, a curl call, a chat front end, an IDE extension config or the Anthropic SDK: point at the loopback base URL and rename the model. The one route that needs no guessing is gemini_cli/, tied to the bullet about custom providers not available elsewhere, including Gemini CLI, and authenticating through OAuth instead of a key.

## requirements.txt installs a folder that lives inside the repository

One line in the dependency file is not a package name: -e src/rotator_library, an editable install of a subdirectory, described in a comment as installing the local rotator library. The Dockerfile obeys it in a specific order. It copies requirements.txt, copies src/rotator_library, and only then runs pip install --no-cache-dir --user -r requirements.txt, so the local folder has to be on disk before the resolver reaches it. Swap the copy and the install and the image build fails on a line that looks like an ordinary requirements entry. The source install path has the same coupling without the staging:

```bash
git clone https://github.com/Mirrowel/LLM-API-Key-Proxy.git
cd LLM-API-Key-Proxy
python3 -m venv venv
source venv/bin/activate  # Windows: venv\Scripts\activate
pip install -r requirements.txt
python src/proxy_app/main.py
```

## Nothing is version pinned, and the desktop GUI toolkit ships to production

The dependency file lists fastapi, uvicorn, python-dotenv, litellm, filelock, httpx, aiofiles, aiohttp, colorlog, rich, customtkinter and pyinstaller. Not one carries a version. litellm is the load-bearing one, since it is what makes provider translation work and it moves its provider mappings often. Two entries are not server dependencies at all: customtkinter, described in a comment as the GUI for model filter configuration, and pyinstaller, described as for building the executable. The image installs the whole list into a user directory in the builder stage and copies that directory wholesale into the production stage, so a desktop GUI toolkit and a binary packer travel with a headless server. Interpreter drift sits next to it: the image is built on python:3.12-slim in both stages, while the source instructions call plain python3 -m venv, so a local environment ends up on whatever python3 resolves to. The image also sets PYTHONPATH=/app/src and PYTHONDONTWRITEBYTECODE=1, and neither of those appears on the source path.

## The PORT variable remaps the host side and leaves the container alone

The compose file publishes ${PORT:-8000}:8000 and passes one environment variable to the container, SKIP_OAUTH_INIT_CHECK=true. The Dockerfile fixes the other end in the image itself, with CMD python src/proxy_app/main.py --port 8000 and EXPOSE 8000. So setting PORT in the environment moves the port on the host and does nothing inside the container, which is the sensible arrangement, but the readme never states it, and a reader who greps for the port will find three places that disagree in scope. Logging is configured the same way, with the json-file driver capped at three files of 10 MB, next to a logs directory mounted for persistent logging, which is a second destination with its own retention story.

## The image creates two directories and leaves the usage one to the host

The Dockerfile runs mkdir -p logs oauth_creds and stops there. usage is never created inside the image, which is why the compose instructions put two host-side commands first, copy the example env file and make the usage directory:

```bash
cp .env.example .env
mkdir usage
docker compose up -d
```

and why the note says the directory has to exist before startup or usage stats stop persisting. The plain docker run example mounts four paths in a row: .env read only, oauth_creds, logs and usage:

```bash
docker run -d \
  --name llm-api-proxy \
  -p 8000:8000 \
  -v $(pwd)/.env:/app/.env:ro \
  -v $(pwd)/oauth_creds:/app/oauth_creds \
  -v $(pwd)/logs:/app/logs \
  -v $(pwd)/usage:/app/usage \
  -e SKIP_OAUTH_INIT_CHECK=true \
  ghcr.io/mirrowel/llm-api-key-proxy:latest
```

One environment flag moves OAuth out of the container entirely, and the note above it asks you to authenticate locally with the credential tool first and then mount the credentials or export them. The compose file repeats those four mounts, adds restart: unless-stopped and a fixed container name, and keeps one line commented out: an optional mount for antigravity_all_combined.env, a combined credential file that nothing in the visible docs explains. Key discovery follows a naming convention instead of a config block, since the env example derives the provider from the part of the variable name before _API_KEY and lets you number duplicates.

## One bearer token, and the env example says any string will do

PROXY_API_KEY is the whole authentication story for the proxy itself. The example env file calls it a secret key used to authenticate requests to this proxy server and then says it can be any string, and it is sent as a Bearer token in the Authorization header. The default surface is narrow, since the documented base URL is http://127.0.0.1:8000/v1 on loopback. Two documented moves widen it: the tip that command-line arguments such as --host 0.0.0.0 --port 8000 bypass the interactive launcher, and the docker run example publishing 8000:8000, which binds every host interface. Nothing in the visible configuration adds a second factor, and the repo carries 18 open issues.

## The releases are CI build tags, and the license field says NOASSERTION

The Windows quick start sends you to the latest release, and the three releases that exist are dev/build-20260530-2-e1f8843, dev/build-20260530-1-ffa6615 and main/build-20260527-1-af40b91, all published in May 2026 while the last push is dated 2026-09-23. So the download path points at artifacts a CI job named, not at a version you can quote, and they predate the current code by four months. The metadata reports no license while the tree carries a LICENSE file at the root, and the docs the quick start never mentions sit beside it: DOCUMENTATION.md, Deployment guide.md, CONTRIBUTING.md, plus docker-compose.tls.yml and docker-compose.dev.yml. The endpoint table runs from GET / through the two chat routes, count_tokens and embeddings, and the next row stops after a backtick and one letter. There is also an empty TODO left in the readme where the launcher screenshot should go. On Windows the path is three steps, download that latest release, unzip it, and run proxy_app.exe to open an interactive TUI launcher, and the same entry point skips the menu when given arguments such as --host and --port.

## Conclusion

This suits someone already holding several provider keys who wants one local endpoint for tools that accept a custom base URL, and it does not suit anyone whose client hardcodes a model name or who needs a pinned, versioned build. Before adopting it, confirm your client accepts provider/model_name strings, count how many keys you actually want rotating, check whether the token you set for PROXY_API_KEY is strong enough for the network you expose it on, and read DOCUMENTATION.md and the deployment guide, since the readme points at neither.

## FAQ

### What does the Mirrowel LLM-API-Key-Proxy actually put in front of my providers?

A FastAPI application that exposes /v1/chat/completions in OpenAI format and /v1/messages in Anthropic format, so Anthropic SDK clients can reach non-Anthropic providers. A second component is a reusable library for API key management, rotation and failover.

### Do I have to change model names to use Mirrowel LLM-API-Key-Proxy?

Yes. Models must be given as provider/model_name, where the prefix selects the backend, and the Claude Code example goes further by mapping Opus, Sonnet and Haiku onto Gemini and OpenAI models. A client that hardcodes gpt-4o has to be pointed at the proxy instead.

### How do I give Mirrowel LLM-API-Key-Proxy more than one key for the same provider?

Increment the number at the end of the variable name, for example GEMINI_API_KEY_1 and GEMINI_API_KEY_2. The provider name is derived from the part of the variable name before _API_KEY, and keys are discovered from environment variables.

### Where do usage statistics and OAuth credentials live in Mirrowel LLM-API-Key-Proxy?

Usage statistics go to the usage directory and OAuth credentials to oauth_creds, and both are mounted from the host. The image creates logs and oauth_creds but not usage, which is why the usage directory has to exist before startup.

### Is there a versioned release of Mirrowel LLM-API-Key-Proxy to pin?

The three published releases are CI build tags named dev/build-20260530-2-e1f8843, dev/build-20260530-1-ffa6615 and main/build-20260527-1-af40b91, from May 2026. The last push is dated 2026-09-23, and no semantic version appears anywhere.

## Sources

- [Issues](https://github.com/Mirrowel/LLM-API-Key-Proxy/issues)
- [Mirrowel/LLM-API-Key-Proxy on GitHub](https://github.com/Mirrowel/LLM-API-Key-Proxy)
- [README](https://github.com/Mirrowel/LLM-API-Key-Proxy/blob/main/README.md)
- [Releases](https://github.com/Mirrowel/LLM-API-Key-Proxy/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/mirrowel-llm-api-key-proxy
