A Go service that proxies multiple AI providers behind a single OpenAI-compatible HTTP API. It resolves client-supplied model strings to configured provider-backed models or aliases, forwards requests upstream (translating when needed for non-OpenAI providers), and returns OpenAI-compatible responses.
GET /v1/modelsGET /v1/billing/usageGET /metricsPOST /v1/chat/completions(JSON and SSE streaming)POST /v1/embeddingsforopenai,openai-compatible, andgeminiprovidersPOST /v1/responsesforopenai,openai-compatible,anthropic, andgeminiproviders (JSON and SSE streaming)POST /v1/images/generationsforopenaiandopenai-compatibleprovidersPOST /v1/audio/transcriptionsforopenaiandopenai-compatibleprovidersPOST /v1/audio/speechforopenaiandopenai-compatibleproviders
none– skip inbound authentication (trusted environments only)bearer_static– validate inboundAuthorization: Bearer ...tokens against statically configured client credentials- optional
rate_limiton theauthblock applies a local in-memory request rate limit; inbearer_staticmode it is enforced per authenticated client, and innonemode it is enforced against a shared anonymous bucket - optional
tenantandallowed_modelsonauth.clientlet you attach client identity metadata and enforce a static allow-list of proxy-visible model names - request accounting is tracked in-process by tenant, client, model, operation,
and status over a rolling 24-hour window;
/metricsexposes aggregated usage event counters - optional
loggingconfig controls structured log level and request lifecycle access logging GET /v1/billing/usagereturns aggregated in-process usage summaries. Inbearer_staticmode it is scoped to the caller's tenant when present, otherwise to the caller's client identity. Summaries use the same rolling 24-hour in-process window as local accounting.- optional
provider_healthconfig can use Redis to share transient provider health state across instances; without it, health remains in-process only
openai– built-in OpenAI adapter (pass-through)openai-compatible– any OpenAI-compatible endpoint (requiresbase_url)anthropic– chat and responses translation to Anthropic Messages APIgemini– chat translation to Gemini generateContent API, embeddings translation to Gemini embedContent API, and responses translation through generateContent
Pass-through providers rewrite only the top-level model JSON field and preserve other request fields, including unknown extension fields. Translated providers reject unsupported top-level request controls rather than silently dropping them; their supported chat fields are model, messages, max_tokens, temperature, top_p, and stream, their supported responses fields are model, input, instructions, max_output_tokens, temperature, top_p, and stream, and Gemini embeddings supports model, input, and dimensions.
- Direct model addressing:
<provider-name>/<model-name> - Alias addressing:
alias/<alias-name> - An alias is a virtual model backed by one or more concrete provider/model targets
- Alias algorithms:
round_robin,least_connections - Alias failover: retry next target on transport errors and upstream
5xxonly;4xxclient errors are returned verbatim
- Quotas, billing, tenancy
The server supports live config reload on SIGHUP for runtime request-routing
state such as auth, providers, models, aliases, and metrics-backed inventory.
Listener address, listener timeout, log-level, and dashboard enablement changes
still require a restart. Unchanged rate-limit settings preserve existing buckets;
changed rate-limit settings reset limiter state.
See docs/design.md for the full design document.
asdf plugin add aiproxy
# or
asdf plugin add aiproxy https://github.com/egose/aiproxy.gitInstall and activate a version:
asdf list all aiproxy
asdf install aiproxy <version>
asdf install aiproxy latest
asdf global aiproxy <version>Once installed, the aiproxy binary is available directly on your PATH:
aiproxy serve
aiproxy validate
aiproxy configure
aiproxy configure provider
aiproxy serve --config /etc/aiproxy/config.hcl
aiproxy validate --config /etc/aiproxy/config.hcl
aiproxy versionBy default, the CLI looks for the config file at $XDG_CONFIG_HOME/aiproxy/config.hcl,
falling back to ~/.config/aiproxy/config.hcl when XDG_CONFIG_HOME is unset.
Pass --config to use a different file.
listener "http" "public" {
address = ":8080"
}
upstream_header_timeout = "120s"
auth "main" {
mode = "none"
}
logging {
level = "info"
access_log = true
}
provider "openai" "openai" {
display_name = "OpenAI"
api_key = env("OPENAI_API_KEY")
model "gpt-4o-mini" {
display_name = "GPT-4o mini"
capabilities = ["chat"]
}
model "gpt-4.1" {
display_name = "GPT-4.1"
capabilities = ["chat", "responses"]
}
model "text-embedding-3-large" {
display_name = "text-embedding-3-large"
capabilities = ["embeddings"]
}
}
provider "openai-compatible" "localai" {
display_name = "LocalAI"
base_url = "https://llm.internal/v1"
upstream_header_timeout = "180s"
api_key_ref {
key = "localai"
}
model "qwen3-32b" {
display_name = "Qwen 3 32B"
}
}
alias "chat_default" {
algorithm = "round_robin"
target {
provider = "openai"
model = "gpt-4o-mini"
}
target {
provider = "localai"
model = "qwen3-32b"
}
}aiproxy paths
aiproxy examples
aiproxy configure
aiproxy configure provider
aiproxy configure provider --config /etc/aiproxy/config.hcl --non-interactive --name backup --type openai-compatible --base-url https://llm.internal/v1 --secrets-key localai --api-key "$LOCALAI_API_KEY" --model qwen3-32b
aiproxy serve --config /etc/aiproxy/config.hcl
aiproxy validate --config /etc/aiproxy/config.hcl
aiproxy versionUse the built-in configure wizard to create or update the HCL and secrets file without editing blocks by hand.
Interactive flows:
aiproxy configure
aiproxy configure provider
aiproxy configure auth
aiproxy configure alias
aiproxy configure upstream
aiproxy configure loggingThe wizard prompts for the config path first when --config is not provided,
defaulting to $XDG_CONFIG_HOME/aiproxy/config.hcl and falling back to
~/.config/aiproxy/config.hcl.
Supported block workflows:
listenerauthprovideraliasupstreamloggingprovider-health
Block subcommands also support non-interactive scripting with flags:
aiproxy configure provider \
--config /etc/aiproxy/config.hcl \
--non-interactive \
--name backup \
--type openai-compatible \
--display-name "Backup provider" \
--base-url https://llm.internal/v1 \
--upstream-header-timeout 180s \
--secrets-path /etc/aiproxy/keys.json \
--secrets-key localai \
--api-key "$LOCALAI_API_KEY" \
--model qwen3-32b=qwen/qwen3-32b \
--model-capabilities qwen3-32b=chat,responses
aiproxy configure alias \
--config /etc/aiproxy/config.hcl \
--non-interactive \
--name chat_default \
--algorithm round_robin \
--target primary/gpt-4o-mini \
--target backup/qwen3-32bUse upstream_header_timeout to control how long the proxy waits for upstream response headers. Provider values override the root value; otherwise the default is 90 seconds. This timeout does not cap response bodies or SSE streams after headers arrive.
Delete existing blocks with --delete:
aiproxy configure provider --config /etc/aiproxy/config.hcl --delete --name backup
aiproxy configure alias --config /etc/aiproxy/config.hcl --delete --name chat_defaultdocker build -t aiproxy .
docker run --rm \
-p 8080:8080 \
-v ./config.hcl:/etc/aiproxy/config.hcl:ro \
-e OPENAI_API_KEY=... \
aiproxyCreate config.hcl then:
go run ./cmd/aiproxy serve --config config.hclgo test ./... # unit tests
make vet test # vet + unit tests
make test-race # unit tests with the race detectorThe repo also includes stub-backed end-to-end tests that run as part of the normal Go test suite. These use in-process HTTP test servers as upstream providers so the full request path can be exercised without external services.
Integration tests are intentionally skipped for now. The repo does not yet ship sandbox services for stable end-to-end provider testing. Reintroduce integration coverage once the sandbox stack is added.
Use env("VAR") in any string attribute in the HCL config to inline an
environment variable. This is necessary for secrets — do not commit secret
values into the config file.
When a provider uses api_key_ref, the proxy reads the key from a JSON file
mapping string keys to string API keys:
{
"openai": "sk-...",
"localai": "secret"
}The file path defaults to $XDG_CONFIG_HOME/aiproxy/keys.json, falling back to
~/.config/aiproxy/keys.json when XDG_CONFIG_HOME is unset. Override it per
provider with api_key_ref { path = "..." key = "..." }.
The optional metrics block governs Prometheus metric exposure. When present,
GET /metrics requires Authorization: Bearer <token> with the configured
token; the token is checked independently from API auth client tokens.
metrics {
token = env("AIPROXY_METRICS_TOKEN")
}- The block is optional. When absent,
/metricsis not exposed. - An empty token is rejected at config validation; metrics are never exposed without a dedicated credential.
- API auth clients (
auth.clientblocks) cannot scrape/metricswith their own tokens — they are checked against the metrics token separately.
The optional dashboard block enables the aiproxy dashboard TUI and the
/_internal/dashboard/* HTTP endpoints on the proxy listener.
dashboard {
token = env("AIPROXY_DASHBOARD_TOKEN")
allow_insecure_remote = false
}tokenis optional. When omitted,aiproxy servemints a random secret at startup and persists it to$XDG_CONFIG_HOME/aiproxy/dashboard.token; thedashboardcommand reads that file to authenticate. Declared tokens are used as-is and the file is not written.allow_insecure_remote(optional, defaultfalse) authorizes the dashboard command to talk to a non-loopback plain-HTTP listener. Whentrue,tokenmust be declared explicitly in config and be at least 32 characters long; a minted or weak token is rejected at validation. HTTPS listeners always satisfy the transport check.
Use the optional enabled field on a provider block to intentionally disable
a provider. Disabled providers are structurally validated (name, type, base
URL, models, capabilities) but do not require a usable api_key or
api_key_ref. Enabled providers with an unresolved, empty, or missing
credential fail validation.
provider "openai" "backup" {
enabled = false
model "gpt-4o-mini" {}
}upstream_name(optional onmodelblocks) lets the proxy-visible model name differ from the exact string sent upstream; it defaults to the model block label.capabilities(optional onmodelblocks) lets you narrow the effective API surface for a model. Supported values arechat,responses,embeddings,images,audio_transcriptions, andaudio_speech.- Provider and alias names are lowercase and must not contain spaces or
/; provider namealiasis reserved. Model names follow the same lowercase rule and may contain/when every slash-separated segment is valid. - Provider
base_urlvalues must be absolutehttpsURLs for remote upstreams. Plainhttpis accepted only for loopback development endpoints. - A provider with no resolved credential, including an empty
api_key = env("..."), fails validation when the provider is enabled. To intentionally disable a provider, setenabled = false; disabled providers are still structurally validated (name, type, base URL, models, capabilities) but do not require a usable credential. /v1/modelsreturns effective capabilities for both direct models and aliases. Alias capabilities are the safe intersection of their target models./v1/modelsalso includes richer metadata:- direct models include
display_nameandprovider_type - aliases include
alias_targetssummaries with provider, model, and resolved display name
- direct models include
- Alias
least_connectionsselection is per-process and best-effort; it is not coordinated across multiple proxy instances. - Provider health state is shared in-process across requests and aliases.
Transient transport failures and upstream
5xxresponses temporarily mark a provider unhealthy for routing and readiness decisions, but this state is not coordinated across multiple proxy instances unlessprovider_health.redis_urlis configured. Whenprovider_health.redis_urlis configured and a Redis read fails, routing and readiness fall back to a bounded in-process cache (provider_health.cache_ttl, default 30s) and fail open only when no fresh cache entry exists; the fallback and the underlying backend error are recorded as Prometheus metrics. - Direct
<provider>/<model>requests do not fail over to other targets. - Alias requests retry the next target only on transport errors, timeouts, and
upstream
5xx; upstream4xxresponses are returned to the client verbatim. - Anthropic providers are translated through the Messages API for both JSON and SSE streaming chat completions.
- Gemini providers are translated through
generateContentandstreamGenerateContent?alt=ssefor JSON and SSE streaming chat completions. POST /v1/embeddingsis currently implemented foropenai,openai-compatible, andgeminiproviders. Requests targetinganthropicmodels return a client-visible unsupported-operation error.POST /v1/images/generationsis currently implemented foropenaiandopenai-compatibleproviders. Requests targeting translated providers return a client-visible unsupported-operation error.POST /v1/audio/transcriptionsis currently implemented foropenaiandopenai-compatibleproviders. Requests targeting translated providers return a client-visible unsupported-operation error.POST /v1/audio/speechis currently implemented foropenaiandopenai-compatibleproviders. Requests targeting translated providers return a client-visible unsupported-operation error.POST /v1/responsesis currently implemented foropenai,openai-compatible,anthropic, andgeminiproviders. The translated provider path supports a conservative request subset for both JSON and streaming responses./metricsexposes Prometheus-format metrics for provider selection, alias retries, skipped providers, readiness state, startup inventory gauges for build version / auth mode / provider types / alias algorithms, explicit readiness reason gauges, inbound HTTP request counts / latency by method and path, request / response body size histograms, streaming response counts / duration, proxy-generated HTTP error counts by endpoint and error type, alias in-flight request gauges by target, provider health gauges, upstream request counts / latency / response body size by operation and provider, provider health backend error counts, and provider health fallback counts by operation and reason./metricsrequires a dedicated bearer token declared in ametrics { token = env("...") }block;GET /metricswithout a validAuthorization: Bearer <token>header returns401. The metrics token is independent of API auth client tokens.- API keys and client bearer tokens are never logged.
- The interactive
aiproxy dashboardcommand calls/_internal/dashboard/*on the same listener as the proxy API. Plain HTTP dashboard RPC is allowed only when the effective listener address is loopback; non-loopback plain HTTP is rejected unlessdashboard { allow_insecure_remote = true }is set with a strong explicittoken(at least 32 characters) declared in config. HTTPS listeners are always allowed. Repeated invalid dashboard tokens are rate limited with429and aRetry-Afterheader.
See the "Deferred Features" section in docs/design.md for the full list, including image and audio APIs, rate limiting, and hot config reload.