No description
  • Go 96.7%
  • Makefile 1.7%
  • Dockerfile 1.6%
Find a file
2026-05-23 05:50:46 +00:00
config feat(core): enhance proxy robustness, observability, and operational features 2026-05-13 16:34:29 +00:00
database feat(api): implement multi-provider routing and model aliasing 2026-05-13 17:20:16 +00:00
docs feat(api): implement multi-provider routing and model aliasing 2026-05-13 17:20:16 +00:00
handlers feat(api): implement multi-provider routing and model aliasing 2026-05-13 17:20:16 +00:00
middleware feat pool 2026-05-23 05:50:46 +00:00
models feat pool 2026-05-23 05:50:46 +00:00
utils feat pool 2026-05-23 05:50:46 +00:00
.dockerignore feat(core): enhance proxy robustness, observability, and operational features 2026-05-13 16:34:29 +00:00
.env.example feat(core): enhance proxy robustness, observability, and operational features 2026-05-13 16:34:29 +00:00
.gitignore feat: implement OpenAI-compatible proxy service 2026-05-12 17:01:21 +00:00
docker-compose.yml feat pool 2026-05-23 05:50:46 +00:00
Dockerfile feat pool 2026-05-23 05:50:46 +00:00
go.mod feat: implement OpenAI-compatible proxy service 2026-05-12 17:01:21 +00:00
go.sum feat: implement OpenAI-compatible proxy service 2026-05-12 17:01:21 +00:00
main.go feat(core): enhance proxy robustness, observability, and operational features 2026-05-13 16:34:29 +00:00
Makefile feat(core): enhance proxy robustness, observability, and operational features 2026-05-13 16:34:29 +00:00
README.md feat pool 2026-05-23 05:50:46 +00:00
SYSTEM.md feat: implement OpenAI-compatible proxy service 2026-05-12 17:01:21 +00:00

go-proxy

OpenAI SDK compatible proxy built on Go Fiber. Forwards /v1/chat/completions and /v1/models to an upstream OpenAI-compatible endpoint, enforces Bearer-token API keys with per-key token limits and expiry, counts tokens with tiktoken-go, injects a configurable system prompt from SYSTEM.md, supports multi-provider routing with model aliasing, and normalizes every upstream/downstream failure into a single error envelope.

Multi-provider + model aliasing

Every API key is bound to a Provider (url + api_key). Models are stored as local_name to provider_model mappings, scoped to a provider. The flow:

  • GET /v1/models reads from provider_models filtered by the caller's provider_id. Clients only ever see configured local_names. No upstream call is made.
  • POST /v1/chat/completions resolves local_name -> provider_model for the caller's provider, swaps the field in the outgoing payload, calls <provider.url>/chat/completions with provider.api_key, and rewrites the model field in the response (and every SSE chunk) back to local_name before sending it to the client.

Operators populate the data with plain SQL or any GORM tool. The proxy seeds a single default provider from EXTERNAL_OPENAI_ROUTE/EXTERNAL_OPENAI_KEY on the first run so a fresh install is usable, but no models are seeded; insert rows into provider_models to expose anything.

Setup

  1. Copy env and edit values:

    Copy-Item .env.example .env
    
  2. Install deps and run:

    go mod tidy
    go run .
    

    On first start the service seeds one API key and prints it in the log. Use it as the client Authorization: Bearer <key> token.

Routes

All routes require Authorization: Bearer <apiKey> except the public docs endpoints.

  • GET /docs Swagger UI (public)
  • GET /openapi.yaml raw OpenAPI 3.1 spec (public)
  • GET /health liveness + database ping
  • GET /info metadata of the caller's API key (never returns the raw key)
  • GET /v1/models proxy to upstream /models
  • POST /v1/chat/completions proxy with streaming and non-streaming support. Token usage is counted and written back to the caller's key.

API key model

Stored via GORM (models.ApiKey, table api_keys). The SQL columns token_limit and token_usage avoid reserved words so the schema works on SQLite, MySQL, and PostgreSQL without provider-specific quoting.

field column type notes
id id varchar(36) primary key, auto-generated uuid
apiKey api_key varchar(128) unique, used as Bearer token
providerId provider_id varchar(36) FK -> providers.id, required
limit token_limit bigint 0 means unlimited
usage token_usage bigint running token total
expiredAt expired_at datetime? null means never expires
createdAt created_at datetime auto set
lastUsedAt last_used_at datetime? updated (coalesced, async) on auth

Provider (models.Provider, table providers)

field column type notes
id id varchar(36) primary key, uuid
name name varchar(128) unique
url url varchar(512) upstream base URL, e.g. https://api.openai.com/v1
apiKey api_key varchar(512) bearer token sent upstream
createdAt created_at datetime auto
updatedAt updated_at datetime auto

ProviderModel (models.ProviderModel, table provider_models)

field column type notes
id id varchar(36) primary key, uuid
providerId provider_id varchar(36) FK -> providers.id
localName local_name varchar(128) what clients send as model
providerModel provider_model varchar(128) what upstream receives as model
createdAt created_at datetime auto
updatedAt updated_at datetime auto

(provider_id, local_name) is unique.

Database providers

Select a provider via DATABASE_PROVIDER. Supported values: sqlite (default), mysql, postgres (alias postgresql).

  • sqlite: uses pure-Go glebarez/sqlite, no CGO needed. DATABASE_DSN or DATABASE_PATH can be a file path (defaults to go-proxy.db).
  • mysql: DATABASE_DSN example: user:pass@tcp(127.0.0.1:3306)/goproxy?charset=utf8mb4&parseTime=True&loc=Local.
  • postgres: DATABASE_DSN example: host=127.0.0.1 user=postgres password=postgres dbname=goproxy port=5432 sslmode=disable.

MySQL and PostgreSQL require DATABASE_DSN. SQLite is used as the fallback when the provider is missing or unknown.

Error envelope

Upstream OpenAI errors are never forwarded as-is. Every failure (auth, upstream, proxy-internal, network) is rewritten into a stable envelope:

{
  "error": {
    "message": "Invalid API key",
    "type": "unauthorized",
    "code": "invalid_upstream_key"
  }
}

Mappings (non-exhaustive):

upstream / source proxy status type code
upstream 401 401 unauthorized invalid_upstream_key
upstream 403 403 forbidden upstream_forbidden
upstream 404 404 not_found resource_not_found
upstream 429 429 rate_limit upstream_rate_limited
upstream 413 413 invalid_request payload_too_large
upstream 400 400 invalid_request invalid_request / upstream code
upstream 422 422 invalid_request unprocessable_request
upstream other 4xx upstream client_error upstream_client_error
upstream 502 / 503 / 504 502 upstream_unavailable upstream_unavailable
upstream 5xx 502 upstream_error upstream_error
cannot reach upstream 502 upstream_unreachable upstream_unreachable
proxy-side failure 500 internal_error internal_error
malformed client request 400 invalid_request invalid_request
missing / invalid bearer token 401 unauthorized invalid_api_key
expired key 403 forbidden key_expired
key without provider_id 403 forbidden key_missing_provider
over token limit 429 rate_limit limit_exceeded
unknown model for key's provider 404 not_found model_not_found
unknown route 404 not_found route_not_found

Env

variable default notes
EXTERNAL_OPENAI_ROUTE https://api.openai.com/v1 trailing slash is stripped
EXTERNAL_OPENAI_KEY (required) bearer token used to call upstream
PORT 3000
SYSTEM_PROMPT_PATH SYSTEM.md
BODY_LIMIT_BYTES 16777216 (16 MiB) request body cap
READ_TIMEOUT 30s Go duration
WRITE_TIMEOUT 10m streamed completions can run long
IDLE_TIMEOUT 2m keep-alive idle timeout
SHUTDOWN_TIMEOUT 20s grace period on SIGINT / SIGTERM
TRUSTED_PROXIES (empty) comma-separated IPs/CIDRs allowed to set X-Forwarded-*
DATABASE_PROVIDER sqlite sqlite / mysql / postgres
DATABASE_DSN (required for mysql / postgres)
DATABASE_PATH go-proxy.db (sqlite only) DSN overrides this if set

Tests

go test ./...

Or with the Makefile (Linux/macOS/WSL/Git Bash):

make test
make cover

Docker

docker build -t go-proxy:latest .
docker run --rm -p 3000:3000 \
  -e EXTERNAL_OPENAI_KEY=sk-... \
  -v "$(pwd)/data:/data" \
  go-proxy:latest

The image is built on distroless/static and runs as a non-root user.

Docker Compose

EXTERNAL_OPENAI_KEY=sk-... docker compose up -d

Configuration is pulled from a .env file in the project root (copy from .env.example). Only EXTERNAL_OPENAI_KEY is required; everything else has sensible defaults. SQLite data is persisted in the go-proxy-data Docker volume.

Operational notes

  • Graceful shutdown. SIGINT / SIGTERM drains in-flight requests for up to SHUTDOWN_TIMEOUT before closing the database pool.
  • Connection reuse. Upstream HTTP traffic uses a shared *http.Transport, so HTTP/2, TLS session resumption, and idle-connection pooling are preserved across requests.
  • last_used_at coalescing. Per-key last_used_at updates are throttled to one write every 30 seconds and run asynchronously, so a request burst does not translate into a write burst on the database.
  • Seeded API key. The default key is logged exactly once on the first start. Recover it later by querying the api_keys table; the proxy never logs it again on subsequent restarts.