Skip to content

dev2k6/LLMProxyAS

Repository files navigation

LLMProxyAS

Go License: MIT Docker OpenAI API GitHub

OpenAI-compatible LLM reverse proxy written in Go.

Repository: https://github.com/dev2k6/LLMProxyAS

Point any OpenAI SDK or HTTP client at LLMProxyAS. The proxy validates models against an allow-list, injects upstream credentials when needed, and forwards traffic to any OpenAI-compatible provider — OpenAI, Azure OpenAI, Groq, Together, DeepSeek, vLLM, Ollama, and more.

Default port 20182
Client base URL http://<host>:20182/v1
Health GET /health
Entrypoint cmd/server
Config configs/config.json, configs/allowed_models.json
License MIT
GitHub dev2k6/LLMProxyAS

Table of contents


Features

Area Details
OpenAI-compatible API Chat completions, embeddings, legacy completions, models list; passthrough for images, audio, moderations
Streaming Full SSE (stream=true) with flush, idle timeout, and max duration
Tools & vision Bodies forwarded as-is (function calling, image_url, multimodal messages)
Model allow-list Only IDs in allowed_models.json are accepted; GET /v1/models reflects that list
Credential injection Upstream api_key from config so apps can call the proxy without a provider key
Provider switch Change base_url + api_key only — no rebuild for config-only deploys with volume mounts
Resilience Panic recovery, body caps, dial/TLS/header timeouts, stream guards, graceful shutdown
Ops JSON structured logs, Docker multi-stage image, Compose healthcheck, non-root container user

How it works

┌──────────────┐   OpenAI HTTP API    ┌──────────────┐    HTTPS     ┌─────────────────┐
│  Your app    │ ───────────────────► │  LLMProxyAS  │ ───────────► │ Upstream LLM    │
│  SDK / curl  │   :20182/v1/...      │  (this repo) │              │ OpenAI / Groq…  │
└──────────────┘                      └──────────────┘              └─────────────────┘
                                            │
                    ┌───────────────────────┼───────────────────────┐
                    │                       │                       │
              model allow-list      API key injection         timeouts / SSE
              (local filter)        (config or client)        logging / recovery
  1. Client uses base_url = http://host:20182/v1 (any official OpenAI SDK works).
  2. Proxy checks model against allowed_models.json (when present in the body).
  3. Proxy forwards to base_url from config.json, attaching authentication.
  4. Response — including SSE chunks — is returned to the client.

Requirements

Mode Requirement
Local binary Go 1.22+, network access to upstream
Docker Docker Engine + Docker Compose v2
Upstream OpenAI-compatible HTTP API; API key if the provider requires one

Quick start

1. Clone and configure

git clone https://github.com/dev2k6/LLMProxyAS.git
cd LLMProxyAS

cp configs/config.example.json configs/config.json

Edit configs/config.json:

{
  "base_url": "https://api.openai.com/v1",
  "api_key": "sk-your-real-upstream-key",
  "port": 20182,
  "log_level": "info"
}

Edit configs/allowed_models.json to the models you allow.

Never commit real API keys. Keep secrets in local files, env-mounted secrets, or a secret manager.

2. Run

Local

go mod tidy
go run ./cmd/server
# or: make run

Docker Compose

docker compose up -d --build
# or: make docker-up

3. Verify

curl -s http://localhost:20182/health
curl -s http://localhost:20182/v1/models

Expected health payload:

{
  "status": "ok",
  "service": "LLMProxyAS"
}

4. First chat call

curl -s http://localhost:20182/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-4o-mini",
    "messages": [{"role": "user", "content": "Hello from LLMProxyAS"}]
  }'

If api_key is set in config, no Authorization header is required from the client.


Authentication

Hop Behavior
Client → LLMProxyAS No proxy API key required. Any client that can reach the port may call the API.
LLMProxyAS → upstream Forwards client Authorization if present; otherwise sends Bearer <api_key> from config.

Recommended pattern

  1. Store the real provider key only in configs/config.json (or a mounted secret).
  2. Point internal apps at http://llmproxyas:20182/v1.
  3. Clients use a dummy SDK key (e.g. not-needed) when the SDK requires the field.

Production warning: LLMProxyAS does not authenticate callers. Expose it only on trusted networks, or put an authenticating gateway (API key, mTLS, OAuth2, IP allow-list) in front. See SECURITY.md.


Configuration

CLI flags

Flag Default Description
-config configs/config.json Upstream and server settings
-models configs/allowed_models.json Model allow-list

Path resolution (first existing file wins):

  1. Path passed on the CLI
  2. configs/<basename>
  3. <basename> in the working directory
  4. Same candidates relative to the executable directory

configs/config.json

Template: configs/config.example.json

Field Default Description
base_url (required) Upstream base URL (typically ends with /v1)
api_key "" Upstream key when client omits Authorization
port 20182 Listen port
timeout_seconds 120 Timeout for non-streaming upstream requests
log_level info debug | info | warn | error
log_body false Reserved for future body logging
max_body_bytes 33554432 Max request body (32 MiB; vision-friendly)
response_header_timeout_seconds 60 Max wait for upstream response headers
stream_idle_timeout_seconds 120 Abort stream if upstream is silent this long
stream_max_seconds 600 Hard cap per stream (10 minutes)
read_header_timeout_seconds 10 HTTP server read-header timeout
idle_timeout_seconds 120 Keep-alive idle timeout

Example full config:

{
  "base_url": "https://api.openai.com/v1",
  "api_key": "sk-xxxxxxxxxxxxxxxx",
  "timeout_seconds": 120,
  "port": 20182,
  "log_level": "info",
  "log_body": false,
  "max_body_bytes": 33554432,
  "response_header_timeout_seconds": 60,
  "stream_idle_timeout_seconds": 120,
  "stream_max_seconds": 600,
  "read_header_timeout_seconds": 10,
  "idle_timeout_seconds": 120
}

configs/allowed_models.json

{
  "models": [
    "gpt-4o",
    "gpt-4o-mini",
    "gpt-4-turbo",
    "claude-3-5-sonnet-20241022",
    "deepseek-chat",
    "qwen2.5-72b-instruct"
  ]
}
Rule Result
model not in list HTTP 400, error.code = model_not_allowed
GET /v1/models Returns only allow-listed IDs (owned_by: LLMProxyAS)
GET /v1/models/{id} unknown HTTP 404, model_not_found

Provider examples

OpenAI

{
  "base_url": "https://api.openai.com/v1",
  "api_key": "sk-..."
}

Groq

{
  "base_url": "https://api.groq.com/openai/v1",
  "api_key": "gsk_..."
}

DeepSeek

{
  "base_url": "https://api.deepseek.com/v1",
  "api_key": "sk-..."
}

Local OpenAI-compatible (vLLM / Ollama / LiteLLM)

{
  "base_url": "http://127.0.0.1:8000/v1",
  "api_key": "ollama"
}

Update allowed_models.json to match the upstream model IDs. Restart the process after config changes (Compose volume mounts need container restart, not rebuild).

Docker Compose environment

Optional .env.example.env:

Variable Default Description
HOST_PORT 20182 Host port mapped to container 20182
IMAGE_TAG latest Image tag
TZ UTC Container timezone

HTTP API

Method Path Auth filter Description
GET /health Liveness
GET /v1/models List allow-listed models (local)
GET /v1/models/{id} Get one allow-listed model
POST /v1/chat/completions model filter Chat, tools, vision, stream
POST /v1/completions model filter Legacy completions
POST /v1/embeddings model filter Embeddings
POST /v1/images/generations model filter* Images
POST /v1/images/edits model filter* Image edits
POST /v1/images/variations model filter* Image variations
POST /v1/audio/speech model filter* TTS
POST /v1/audio/transcriptions model filter* STT
POST /v1/audio/translations model filter* Audio translation
POST /v1/moderations model filter* Moderations

* Model filter applies when the JSON body includes a non-empty model field.

Unsupported or future /v1/* POST routes may still be forwarded via the catch-all handler with the same model filter rules.


Client examples

Python (OpenAI SDK)

from openai import OpenAI

client = OpenAI(
    base_url="http://localhost:20182/v1",
    api_key="not-needed",
)

resp = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "Hello"}],
)
print(resp.choices[0].message.content)

Streaming

stream = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "Count to five"}],
    stream=True,
)
for chunk in stream:
    if chunk.choices and chunk.choices[0].delta.content:
        print(chunk.choices[0].delta.content, end="", flush=True)

Tools / function calling

tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Get weather by city",
        "parameters": {
            "type": "object",
            "properties": {"city": {"type": "string"}},
            "required": ["city"],
        },
    },
}]

resp = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "Weather in Hanoi?"}],
    tools=tools,
)
print(resp.choices[0].message)

Vision (image URL)

resp = client.chat.completions.create(
    model="gpt-4o",
    messages=[{
        "role": "user",
        "content": [
            {"type": "text", "text": "What is in this image?"},
            {"type": "image_url", "image_url": {"url": "https://example.com/cat.jpg"}},
        ],
    }],
)

Node.js

import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "http://localhost:20182/v1",
  apiKey: "not-needed",
});

const resp = await client.chat.completions.create({
  model: "gpt-4o-mini",
  messages: [{ role: "user", content: "Hello" }],
});
console.log(resp.choices[0].message.content);

cURL

# Models
curl -s http://localhost:20182/v1/models | jq .

# Chat (key optional if configured server-side)
curl -s http://localhost:20182/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-4o-mini",
    "messages": [{"role": "user", "content": "Hi"}]
  }' | jq .

# Stream
curl -N http://localhost:20182/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-4o-mini",
    "stream": true,
    "messages": [{"role": "user", "content": "Say hello slowly"}]
  }'

Rejected model (example)

curl -s http://localhost:20182/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"not-allowed","messages":[{"role":"user","content":"x"}]}'
{
  "error": {
    "message": "model 'not-allowed' is not allowed by this proxy; see GET /v1/models for permitted models",
    "type": "invalid_request_error",
    "param": "model",
    "code": "model_not_allowed"
  }
}

Docker

Compose (recommended)

# Configure configs/config.json and allowed_models.json first
docker compose up -d --build

docker compose ps
docker compose logs -f
curl -s http://localhost:20182/health

docker compose down
Item Value
URL http://localhost:20182
Health http://localhost:20182/health
Config volume ./configs/app/configs (read-only)
Image llmproxyas:latest
User non-root (appuser, uid 10001)

Custom host port:

# bash
HOST_PORT=8080 docker compose up -d --build

# PowerShell
$env:HOST_PORT = "8080"; docker compose up -d --build

Build & run image

docker build -t llmproxyas:latest .

# Linux / macOS
docker run --rm -p 20182:20182 \
  -v "$(pwd)/configs:/app/configs:ro" \
  llmproxyas:latest

# Windows PowerShell
docker run --rm -p 20182:20182 `
  -v "${PWD}/configs:/app/configs:ro" `
  llmproxyas:latest

Docker files

File Purpose
Dockerfile Multi-stage Go build → Alpine runtime, healthcheck
docker-compose.yml Ports, volume, restart, log rotation, healthcheck
.dockerignore Lean, secret-safe build context
.env.example Optional Compose variables

Prebuilt binaries

Release binaries are published in the bin/ directory of this repository for convenience:

File Platform
bin/LLMProxyAS.exe Windows x64 (shortcut)
bin/LLMProxyAS-windows-amd64.exe Windows x64
bin/LLMProxyAS-windows-arm64.exe Windows ARM64
bin/LLMProxyAS-linux-amd64 Linux x64
bin/LLMProxyAS-linux-arm64 Linux ARM64
bin/LLMProxyAS-darwin-amd64 macOS Intel
bin/LLMProxyAS-darwin-arm64 macOS Apple Silicon

Also included: bin/configs/ (config.json, allowed_models.json) and bin/README.txt.

Run (Windows example) — execute from bin/ so local configs resolve:

cd bin
.\LLMProxyAS.exe

Edit bin/configs/config.json (api_key, base_url) before production use.

Rebuild all platforms:

powershell -ExecutionPolicy Bypass -File .\scripts\build-all.ps1

Direct download (raw GitHub):

https://github.com/dev2k6/LLMProxyAS/raw/main/bin/LLMProxyAS-windows-amd64.exe
https://github.com/dev2k6/LLMProxyAS/raw/main/bin/LLMProxyAS-linux-amd64
https://github.com/dev2k6/LLMProxyAS/raw/main/bin/LLMProxyAS-darwin-arm64

(Adjust branch name if not main.)

Build from source

go mod tidy

# Linux / macOS
go build -o bin/LLMProxyAS ./cmd/server
./bin/LLMProxyAS -config configs/config.json -models configs/allowed_models.json

# Windows
go build -o bin/LLMProxyAS.exe ./cmd/server
.\bin\LLMProxyAS.exe -config configs\config.json -models configs\allowed_models.json

Cross-compile example (Linux binary from any host with Go):

GOOS=linux GOARCH=amd64 CGO_ENABLED=0 \
  go build -trimpath -ldflags="-s -w" -o bin/LLMProxyAS ./cmd/server

Or build every target into bin/:

powershell -ExecutionPolicy Bypass -File .\scripts\build-all.ps1

Makefile targets

Target Description
make build Build binary into bin/
make run go run ./cmd/server
make tidy go mod tidy
make test go test ./...
make clean Remove build artifacts
make docker-build docker compose build
make docker-up Build and start detached
make docker-down Stop stack
make docker-logs Follow logs
make help List targets

Project layout

.
├── cmd/server/                 # Process entrypoint (flags, exit codes)
├── internal/
│   ├── app/                    # DI, lifecycle, config path resolution
│   ├── config/                 # config.json + allowed_models loaders
│   ├── handler/                # Proxy, models, health handlers
│   ├── httpserver/             # Gin router + http.Server wrapper
│   ├── logging/                # slog JSON logger
│   ├── middleware/             # Recovery, request log, model filter
│   └── oaierr/                 # OpenAI error JSON helpers
├── configs/
│   ├── config.example.json     # Safe template for distribution
│   ├── config.json             # Local runtime config (keep secrets out of git)
│   └── allowed_models.json     # Model allow-list
├── Dockerfile
├── docker-compose.yml
├── Makefile
├── LICENSE
├── CHANGELOG.md
├── CONTRIBUTING.md
├── SECURITY.md
└── README.md
Package Responsibility
cmd/server Thin main, CLI flags, fatal recovery
internal/app Wire dependencies; run / signal shutdown
internal/httpserver Routes, listen, graceful stop
internal/handler Reverse proxy and API handlers
internal/middleware Cross-cutting HTTP middleware
internal/config Load and validate JSON configuration
internal/oaierr Safe, consistent client errors
internal/logging Log level and JSON handler setup

Resilience

Concern Behavior
Panic in handler Recovered; OpenAI-style 500 if body not started
Oversized body Rejected (413 / body_too_large)
Upstream dial / TLS Transport-level timeouts
Upstream header stall ResponseHeaderTimeout → gateway error / timeout
Non-stream hang http.Client.Timeout = timeout_seconds
Stream silence stream_idle_timeout_seconds + best-effort SSE error event
Stream too long stream_max_seconds context cancel
Client disconnect Upstream request cancelled via request context
Process stop SIGINT / SIGTERM → graceful Shutdown, then force Close
Double-write guard JSON errors skipped if response already started (e.g. mid-SSE)

Logging

  • Library: log/slog with JSON handler to stdout
  • Levels: debug, info, warn, error via log_level
  • Request logs include method, path, status, latency, client IP, model (when set)

Example line:

{
  "time": "2026-07-19T12:00:00Z",
  "level": "INFO",
  "msg": "request",
  "method": "POST",
  "path": "/v1/chat/completions",
  "status": 200,
  "latency_ms": 342,
  "model": "gpt-4o-mini",
  "client_ip": "127.0.0.1"
}

Error format

All proxy-generated errors follow the OpenAI envelope:

{
  "error": {
    "message": "human-readable message",
    "type": "invalid_request_error",
    "param": "model",
    "code": "model_not_allowed"
  }
}

Common code values:

Code Meaning
model_not_allowed Model not in allow-list
model_not_found Model id not listed / unknown
body_too_large Request body over max_body_bytes
upstream_error Upstream request failed
upstream_timeout Upstream timed out
upstream_unreachable Dial / DNS failure
stream_interrupted Stream ended with error after headers were sent
internal_error Unexpected panic / internal failure

Upstream HTTP error bodies are forwarded when the upstream responds with a normal HTTP response.


Security

Summary:

  1. Do not commit secrets — use config.example.json; keep real keys local or in secret stores.
  2. No client authentication — protect the port with network policy or a gateway.
  3. Allow-list models — reduce cost and abuse surface.
  4. Prefer private networks for production deployments.
  5. Container defaults — non-root user, read-only config mount in Compose.

Full policy: SECURITY.md.


Troubleshooting

Symptom Things to check
failed to load config Path to configs/config.json; run from repo root or pass -config
connection refused on client Process/container not listening; wrong host/port
Upstream 401 / 403 Invalid api_key or client-forwarded token
model_not_allowed Add model id to allowed_models.json and restart
Stream stops / idle timeout Increase stream_idle_timeout_seconds or check upstream
Long jobs cut off Increase stream_max_seconds / timeout_seconds
Docker health unhealthy Logs via docker compose logs; ensure port 20182 inside container
Body too large Raise max_body_bytes for large base64 vision payloads

Debug logging:

"log_level": "debug"

Roadmap

Ideas under consideration (not commitments):

  • Client authentication middleware (static keys / bearer)
  • Model alias map (client id → upstream id)
  • Rate limiting and basic quotas
  • Multi-upstream failover
  • Prometheus metrics endpoint
  • Optional request/response body logging (log_body)
  • Hot-reload of allow-list / config

Contributions welcome — see CONTRIBUTING.md.


Contributing

See CONTRIBUTING.md for setup, coding guidelines, and PR expectations.


Changelog

See CHANGELOG.md for release history.


License

This project is licensed under the MIT License — see LICENSE for the full text.

About

LLMProxyAS is an OpenAI-compatible reverse proxy that filters models, injects upstream API keys, and forwards chat, embeddings, and streaming traffic to any OpenAI-compatible provider.

Topics

Resources

License

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Contributors