LLM gateway without bloat.
One small, stateless binary and one TOML file, under a millisecond per request (p99 on c7i.4xlarge), with the features a gateway needs and no more.
client
Works with existing clients. Point Claude Code, Codex, or any app on the official Anthropic or OpenAI SDK at it by base URL; wye claude and wye codex do it for you. Messages, Responses, and Chat Completions pass through untranslated; only a model id your config maps changes. Streams are relayed as the provider sends them, never buffered. (running it, apps)
POST /v1/messages | Claude Code, Anthropic SDKs |
POST /v1/responses | Codex, OpenAI SDKs |
POST /v1/chat/completions | OpenAI SDKs, compatible agents |
access
Gives each caller its own gateway token. It stands in for the provider key and is signed by Wye or your identity provider (GitHub Actions, or SSO through Claude Code's apiKeyHelper). A token names its models and output-token ceiling; anything else is refused before the provider gets the whole request, and Claude Code's model picker can list only those models. (teams, apps, tokens)
{
"iss": "wye", "sub": "alice", "aud": "wye-prod",
"iat": 1794048745, "exp": 1794653545,
"models": ["claude-sonnet-*"], "max_output_tokens": 8000
}model
Changes models in one place. Aliases map a name to a model, per caller (a person, a repository, or a service). Upgrade checks tell you which Messages requests a newer model would reject, before you switch. Wye never rewrites a request to make it fit. (aliases and upgrades)
{ "model": "claude-coding",served as claude-sonnet-5 today "max_tokens": 2048, "thinking": { "type": "enabled", "budget_tokens": 1024budget_tokens: use adaptive thinking }, "temperature": 0.2,sampling: omit it "messages": [ { "role": "user", "content": "Fix it." }, { "role": "assistant",prefill: end with a user turn "content": "Summary:" } ]}upstream
Spreads load across keys and providers. Several Anthropic keys, Claude Platform on AWS, Bedrock, or several OpenAI keys behind one address, split by weight, with failover between them. (several upstreams)
{
"upstream": "bedrock-us",
…
"failovers": [{
"upstream": "anthropic",
"reason": "429"
}]
}stream
Lab results from three fresh AWS c7i.4xlarge hosts, each running Wye and four other gateways in the same session against a mock provider, every gateway pinned to the same two physical cores; one command reproduces them (reference environment). Each cell is the gateway's median divided by Wye's (TLS to the upstream; stream events with 2,000 token-paced streams):
| Wye | 1× | |
|---|---|---|
| KrakenD | 1.4× | |
| agentgateway | 2.8× | |
| Bifrost | 9.7× | |
| LiteLLM | 330× | |
| 1×10×100×1,000× |
| Wye | 1× | |
|---|---|---|
| agentgateway | 3.5× | |
| Bifrost | 468× | |
| KrakenD | 1,433× | |
| LiteLLM | 1,738× | |
| 1×10×100×1,000×10,000× |
Under load the gap widens. With 2,000 token-paced streams, Wye adds 0.28 ms per event at p99; agentgateway adds 1.0 ms, Bifrost 132 ms, LiteLLM 490 ms, and KrakenD, which buffers, 404 ms (results). Under the stress scenarios LiteLLM failed 104-132 requests and streams on every host; Wye failed none, in any scenario (failures).
Where others lead, it is in three places. KrakenD moves bulk transfers faster (3.7 against 2.4 GB/s) because it buffers responses instead of streaming them. agentgateway does better on cold connections and on memory beside clients that stopped reading, with a delay per event close to Wye's. LiteLLM uses less memory per idle stream over plain HTTP. The benchmarks set each vendor's own claim beside these numbers, with the method and the configs.
| What Wye adds | Plain HTTP/1.1 | TLS + HTTP/2 |
|---|---|---|
| Latency per request (p50 / p99) | 254 / 559 µs | 325 / 628 µs |
| CPU per request | 42 µs | 62.5 µs |
| 2,000 concurrent token-paced streams, delay per event (p50 / p99) | not measured | 77 / 282 µs |
| CPU per event and memory per open stream at that load | not measured | 14 µs, 12.6 KB |
| Memory at idle | 28 MB | 28 MB |
Every commit that changes code is benchmarked against its parent, in alternating rounds on the same machine (scripts/check.sh). If it makes Wye slower or heavier beyond the commit gate's tolerances, it doesn't get in.
record
Shows usage and cost. One JSON line per request records its tokens, cost, and model, and for Claude Code and Codex the session, subagent, request class, and tool calls. wye report reads the usage log, no backend needed. With [telemetry], logs, metrics, and one trace per session go to your OpenTelemetry backend. Every prompt-cache miss is recorded with its reason. (reading the report, telemetry, prices)
$ wye report
3 requests in 1 session; outcomes: completed 3
By request class
cache cache time time
class reqs input output read write hit cost model tools
all 3 36 114 149K 4.6K 97.0% $0.0553 14s 1.8s
main 3 36 114 149K 4.6K 97.0% $0.0553 14s 1.8s
...
compare
Most rows follow from two choices, passing native APIs through and keeping nothing between requests.
| Teams usually want | Wye |
|---|---|
| Spend by team, user, and app | Yes. Down to the agent session, subagent, and tool, with prompt-cache misses explained |
| Access per caller instead of the provider key | Yes. A gateway token per developer, CI job, or app |
| Spend caps and rate limits | Partly. Per-request output-token ceilings, and the provider's own limits; Wye keeps no counters. Provider-held budgets per team are planned |
| Failover and load across keys | Yes. Split by weight (configuration) |
| Logs, metrics, and traces | Yes. In your OpenTelemetry backend |
| Switching models centrally | Yes. Aliases and upgrade checks |
| Policy you can review and test | Yes. One TOML file in Git, checked in CI by wye policy-test and wye policy-diff |
| One API for every provider | No. For many providers behind one API, LiteLLM or OpenRouter fit better |
| Guardrails, redaction, response caching | No. What Wye will not do says why |
| Scale out by adding replicas | Yes. Behind any load balancer. The provider keys' rate limits are the ceiling; more keys raise it |
A team that runs only Claude Code with SSO may prefer Anthropic's own self-hosted gateway for Claude Code, which follows every Claude Code release.
Wye passes native APIs through because provider APIs keep changing. Within one year on Anthropic's API, thinking.budget_tokens, sampling parameters, assistant prefill, forced tool_choice, and output_format all broke on newer models. A gateway that translates everything into one format is always catching up (what Wye changes).
config
A route with two upstreams, which fail over to each other, and a price table:
[server]
listen = "127.0.0.1:8080"
[[upstreams]]
id = "anthropic"
weight = 2 # twice the sessions of the other
provider_key = { env = "ANTHROPIC_API_KEY" }
[[upstreams]]
id = "bedrock-us"
provider = "bedrock"
base_url = "https://bedrock-mantle.us-east-1.api.aws/anthropic"
provider_key = { env = "BEDROCK_API_KEY" }
[pricing]
version = "anthropic-2026-11-01"
[[pricing.prices]]
models = ["claude-opus-5-5"]
input = "4.00" # USD per million tokens
output = "20.00"
cache_read = "0.20"
cache_write_5m = "5.00"
cache_write_1h = "8.00"
Each session stays on one upstream. A rejected request moves to the other; two keys of one provider work the same way, and standby = true keeps an upstream for failover only (details). Upstreams of provider anthropic (the default), bedrock, and claude-platform-aws serve Claude's API; openai serves OpenAI's. Secrets come from environment variables or files, never the config. An unknown field stops startup. examples/wye.toml has every field with its default and limits, and a test keeps it valid.
Checks decisions before they ship. wye explain shows what the gateway would do with a request, offline. wye policy-test and wye policy-diff run in CI and fail when a change widens access. wye doctor checks secrets, files, and upstreams without a paid request. (commands)
$ wye explain -c wye.toml --session session-a request.json
config: wye.toml
endpoint: POST /v1/messages
gateway token: not required (no [auth]); a local client: a loopback Host, no Origin
body: 88 bytes (limit 32 MiB)
model: claude-opus-5-5; allowed by config: any; by token: any
selection: upstream bedrock-us of 2 (cache affinity by session id)
failover: on 429, 529, connect to anthropic in that order, for bodies up to 8 MiB
upstream: POST https://bedrock-mantle.us-east-1.api.aws/anthropic/v1/messages
provider key: x-api-key from $BEDROCK_API_KEY
model id: the body names anthropic.claude-opus-5-5 upstream, the provider's id for claude-opus-5-5
price: claude-opus-5-5 at bedrock-us: input $4.00, output $20.00, cache read $0.20, cache write 5m $5.00, cache write 1h $8.00 per million tokens (pricing version anthropic-2026-11-01)
decision: forwardRuns as one process, or many. wye service installs a macOS LaunchAgent or a systemd user unit. Replicas behind any load balancer never talk to each other, and each sends a session to the same upstream, so prompt caches stay warm without sticky sessions. Each has health checks and a graceful shutdown. (deployment)
status
Everything under What it does is built and covered by tests. I've run Claude Code through Wye on a claude.ai subscription since 2026-10-30.
Claude Code with an API key, Codex, Bedrock, and Claude Platform on AWS are tested only against mocks so far. wye service hasn't run under systemd on a real Linux machine.
Next comes a report of which tokens were wasted and what each fix would save, and a smaller model for small background requests, then provider-held budgets per team. A published container image, Vertex AI, and AWS SigV4 are not built. Details in docs/roadmap.md.
will not do
- Keep global rate limits, spending budgets, or quotas. They need shared state, and providers already enforce them per key or workspace.
- Break circuits, cool down upstreams, or balance by measured latency. Each remembers across requests. Failover handles a rejection within the request.
- Translate between provider APIs, rewrite prompts, swap in another model when one fails, or cache responses.
- Filter content (guardrails, redaction) or store prompts.
- Terminate TLS. A load balancer, an ingress, or Caddy in front does it, and renews the certificate.
- Run agents or tools.
- Protect a public endpoint from abuse. Per-user authentication and quotas stay in your backend.
- Grow a dashboard or an admin API. Config changes go through Git and a restart or rolling deploy.