polygate
One function. Any LLM provider.
polygate is a tiny, dependency-light wrapper that gives you a single
consistent interface for calling Anthropic (Claude), OpenAI, Google
Gemini, and Moonshot (Kimi K2), instead of learning four different
SDKs and four different response shapes.
from polygate import chat
response = chat(
provider="anthropic", # or "openai", "gemini", "moonshot"
model="claude-sonnet-4-6",
messages=[{"role": "user", "content": "Say hi in one word."}],
api_key="sk-...", # or set ANTHROPIC_API_KEY in the environment
)
print(response.content) # "Hi!"
print(response.usage) # Usage(prompt_tokens=..., completion_tokens=..., total_tokens=...)
import { chat } from "polygate";
const response = await chat({
provider: "openai",
model: "gpt-4o",
messages: [{ role: "user", content: "Say hi in one word." }],
apiKey: "sk-...", // or set OPENAI_API_KEY in the environment
});
console.log(response.content);
This repo isn't trying to out-feature mature gateways like LiteLLM, Portkey, or Helicone. It's a small, boring, easy-to-read wrapper built to be genuinely simple to depend on and easy to maintain. Contributions are welcome, but the scope is intentionally kept narrow: normalize the request/response shape across providers, plus the two reliability primitives that are impractical to bolt on from outside: key rotation and backoff retries, both opt-in and off by default. Caching and routing are still yours to build on top.
Key rotation and retries
Both are optional. A single key with no retry behaves exactly as it did
before these existed: one request, one response, errors raised straight
through.
from polygate import chat, Retry
response = chat(
provider="anthropic",
model="claude-sonnet-4-6",
messages=[{"role": "user", "content": "Say hi."}],
api_key=["sk-1", "sk-2", "sk-3"], # str | list[str] | KeyPool
retry=Retry(max_attempts=4),
)
A rate limit is per key, not per host, so a 429 rotates to another key before it sleeps. Backing off while a key sits idle wastes the whole window. A key the provider rejects (401/403) is evicted for the rest of the process instead of burning an attempt on every later request. Deterministic failures (400, 404, 422) are never retried.
The provider environment variables also accept a comma-separated list, so rotation can be turned on without a code change.
Why this exists
Most unified-LLM libraries are either full frameworks (with retries,
caching, cost tracking, routing, and a dozen other opinions baked in)
or tightly coupled to one ecosystem (e.g. LangChain). polygate is
neither. It's the smallest possible layer that lets you swap
providers by changing one string, with each provider adapter being
readable in under 100 lines.
Packages
| Language | Package | Install |
|---|---|---|
| Python | polygate |
pip install polygate |
| TypeScript / JavaScript | polygate |
npm install polygate |
Both packages are published at 0.1.0. To run from source instead, see
CONTRIBUTING.md.
Supported providers
| Provider | Aliases | Env var fallback |
|---|---|---|
| Anthropic (Claude) | anthropic, claude |
ANTHROPIC_API_KEY |
| OpenAI | openai, gpt |
OPENAI_API_KEY |
| Google Gemini | gemini, google |
GEMINI_API_KEY |
| Moonshot (Kimi K2) | moonshot, kimi |
MOONSHOT_API_KEY |
The unified message format
Regardless of provider, messages is always a list of:
{ "role": "system" | "user" | "assistant", "content": "..." }
Each adapter translates this into whatever shape the underlying
provider actually expects (e.g. Anthropic's separate system field,
Gemini's contents/model role naming).
The unified response
Every call returns the same shape, regardless of provider:
content: the assistant's reply textrole: always"assistant"model: the model that was actually usedprovider: which provider served the requestusage:{ prompt_tokens, completion_tokens, total_tokens }raw: the untouched, original provider response, for anything not normalized above
Extra parameters
Anything you pass beyond provider, model, messages, api_key,
temperature, and max_tokens is forwarded straight through to the
provider's request payload, so provider-specific options (tools,
top_p, stop sequences, etc.) still work without polygate needing to
know about them ahead of time.
Project layout
polygate/
├── python/ # pip package
├── typescript/ # npm package
├── README.md
├── CONTRIBUTING.md
└── LICENSE
Contributing
See CONTRIBUTING.md. Adding a new provider is usually just: one new adapter file + one line in the registry.
License
MIT. See LICENSE.
Release files for polygate 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| polygate-0.3.0.tar.gz | 46.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| polygate-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 95.7 kB
Release files / polygate-0.3.0.tar.gz
| Download URL | polygate-0.3.0.tar.gz |
|---|---|
| Size | 46.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a1e8ed53aecaf7036a64f018d8c5c83dccf1ddf7567cb9a384059ba736e4a897
|
|
BLAKE2b-256 checksum How to use checksums |
1f3548c858c22802d8b09b147e5d08190bee95a2d321b7d0a0cf639478ea898b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.13
|
Release files / polygate-0.3.0-py3-none-any.whl
| Download URL | polygate-0.3.0-py3-none-any.whl |
|---|---|
| Size | 49.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1a5a1c77c071cf4fa7b9ae75b5812c186aecdb2cf8a5405665141016ac958815
|
|
BLAKE2b-256 checksum How to use checksums |
a43c6d541f6770455b2351e96a8756eea497569e06ed54f30faa126b7a25ac26
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.13
|