freelm is a free, always-up LLM client and gateway for Python (and JS/TS) that pools six free-tier LLM providers — OpenRouter, Google Gemini (AI Studio), NVIDIA NIM, Groq, Cerebras, and Mistral — behind one OpenAI-compatible call (with streaming), with automatic API-key rotation, cross-provider failover, circuit breaking, rate-limit/quota-aware routing, and live free-model discovery. Drop in whichever free keys you have and your app keeps talking to an LLM even when one source rate-limits or goes down.
📦 PyPI: https://pypi.org/project/freelm/ — pip install freelm
🌐 Website & docs: https://shihub.online/freelm · https://shihub.online/freelm/docs
Python + JS/TS (
npm install freelm, lives injs/). A Go port is planned (the core is spec-driven for portability).
LLMs show up in nearly every project, and they cost money — but there's a lot of free capacity scattered across providers:
- OpenRouter — free models (
:free), ~50 req/day under $10 credit, ~1000/day at ≥$10. - Google AI Studio (Gemini) — generous free tier; Tier 1 (billing on) lifts limits hard.
- NVIDIA NIM (
build.nvidia.com) — many models free against build credits. - Groq — 30 RPM / 14,400 req-day free, very fast inference, no card.
- Cerebras — ~30 RPM, 1M tokens/day free (8K context cap), no card.
- Mistral — free "Experiment" tier: 2 RPM, 500K TPM, 1B tokens/month.
freelm pools them behind one fault-tolerant client.
Free-tier numbers above were verified 2026-06 and change often — they're defaults you can override with
tier/rpm/rpd.
pip install freelmJavaScript / TypeScript: npm install freelm — same API, in js/. Zero runtime deps (built-in fetch).
import freelm
llm = freelm.FreeLLM.from_env() # reads keys from environment
print(llm.text("Explain black holes in one sentence."))Explicit config:
from freelm import FreeLLM, OpenRouter, GoogleAIStudio, NIM
llm = FreeLLM(
providers=[
OpenRouter("sk-or-...", tier="free"), # or tier="credit" if ≥ $10
GoogleAIStudio("AIza...", tier="free"), # or tier="tier1"
NIM("nvapi-..."),
],
strategy="quota_aware", # priority | round_robin | quota_aware | latency
)
resp = llm.chat(
[{"role": "user", "content": "Write a haiku about failover."}],
model="chat:fast", # virtual model, see below
)
print(resp.text, "via", resp.provider)Async is symmetric:
from freelm import AsyncFreeLLM
async with AsyncFreeLLM.from_env() as llm:
print(await llm.text("hi"))Token streaming works across every provider and through the same failover. It fails over between providers before the first token; once tokens start flowing it stays on that provider (no mid-stream switching).
llm = freelm.FreeLLM.from_env()
for chunk in llm.stream("Write a haiku about failover."):
print(chunk, end="", flush=True)async with freelm.AsyncFreeLLM.from_env() as llm:
async for chunk in llm.astream("Stream me some tokens"):
print(chunk, end="", flush=True)# from openai import OpenAI
from freelm.compat import OpenAI
client = OpenAI() # backed by FreeLLM.from_env()
r = client.chat.completions.create(
model="auto",
messages=[{"role": "user", "content": "hi"}],
)
print(r.choices[0].message.content)OpenAI-SDK constructor arguments (api_key, base_url, organization, ...) are
accepted and ignored — keys come from the environment. stream=True works and
yields chat.completion.chunk-shaped objects:
for chunk in client.chat.completions.create(model="auto", messages=msgs, stream=True):
print(chunk.choices[0].delta.content or "", end="")| Provider | Key vars (first match wins) | Tier var |
|---|---|---|
| OpenRouter | OPENROUTER_API_KEY / FREELM_OPENROUTER_KEYS |
FREELM_OPENROUTER_TIER (free|credit) |
| Google AI Studio | GEMINI_API_KEY / GOOGLE_API_KEY / GOOGLE_AI_STUDIO_KEY / FREELM_GOOGLE_KEYS |
FREELM_GOOGLE_TIER (free|tier1) |
| NVIDIA NIM | NVIDIA_API_KEY / NIM_API_KEY / FREELM_NIM_KEYS |
FREELM_NIM_TIER (free) |
| Groq | GROQ_API_KEY / FREELM_GROQ_KEYS |
FREELM_GROQ_TIER (free) |
| Cerebras | CEREBRAS_API_KEY / FREELM_CEREBRAS_KEYS |
FREELM_CEREBRAS_TIER (free) |
| Mistral | MISTRAL_API_KEY / FREELM_MISTRAL_KEYS |
FREELM_MISTRAL_TIER (free) |
Multiple keys per provider: comma-separate them. See .env.example.
Groq vs xAI Grok: different companies. Groq (
gsk_…) has a free tier and is supported. xAI Grok (xai-…) is paid, so it's intentionally not included — freelm is free-only.
Names differ per provider, so ask by intent and freelm maps to a concrete model:
| Alias | Meaning |
|---|---|
auto / chat |
any available chat model (priority, then registry order) |
chat:large / large |
a larger/stronger model |
chat:fast / fast |
a fast/cheap model |
chat:small / small |
smallest model |
chat:tools / tools |
models that support function calling |
vision / reasoning |
models tagged with that capability |
vendor/model-id |
passthrough — use exactly this model |
Override the table per provider with models=[ModelSpec(...)].
Three ways to control which model wins, from static to per-call:
from freelm import FreeLLM, OpenRouter, ModelSpec
# 1. ModelSpec(priority=) — order a static list (lower = first)
OpenRouter("sk-or-...", discover=False, models=[
ModelSpec("openai/gpt-oss-120b:free", ("chat", "large"), priority=1),
ModelSpec("meta-llama/llama-3.3-70b-instruct:free", ("chat", "large"), priority=0),
])
# 2. prefer=[...] — bias *discovered* lists without replacing them
# (exact id, else case-insensitive substring; survives refresh_models())
OpenRouter("sk-or-...", prefer=["qwen/qwen3-next-80b-a3b-instruct:free", "gpt-oss"])
# 3. per-call ordered fallback chain — ids and aliases mix freely
llm.chat(msgs, model=["groq-only/llama-3.3-70b-versatile", "chat:fast"])Provider priority= (lower = tried first) is now the universal tiebreak: primary
for strategy="priority", secondary for quota_aware/latency (equal headroom
or latency → lower priority wins), and the baseline order for round_robin.
Free model IDs churn constantly, so freelm doesn't trust its hardcoded list. For OpenRouter (on by default), it queries GET /models on first use, derives tags (large/fast/small, plus tools/vision/reasoning from supported_parameters), and caches the list to disk.
Resolution order: live API → disk cache → hardcoded fallback (so it still works offline / key-less).
from freelm import list_free_models
for m in list_free_models()[:5]: # live OpenRouter free models, cached
print(m.id, m.tags, m.ctx)Control it:
OpenRouter("sk-or-...", discover=True, discover_free_only=True, cache_ttl=3600)
GoogleAIStudio("AIza...", discover=True) # opt-in for other providers' /models
llm.refresh_models() # force re-fetch on next call| Env var | Default | Meaning |
|---|---|---|
FREELM_CACHE_DIR |
~/.cache/freelm |
where the model cache lives (file is 0600) |
FREELM_CACHE_TTL |
3600 |
cache lifetime in seconds |
Client knobs — FreeLLM(...) / AsyncFreeLLM(...):
| Param | Default | What it does |
|---|---|---|
strategy |
"priority" |
how providers are ranked (see below) |
max_attempts |
12 |
hard cap on total tries across all providers/keys/models per call |
timeout |
60.0 |
per-request timeout (s); also the overall deadline for one chat() |
wait |
False |
if every key is cooling, sleep until one frees instead of failing |
max_wait |
20.0 |
longest single sleep (s) when wait=True |
on_event |
None |
observability callback — see below |
persist |
False / FREELM_PERSIST |
carry quota/cooldown/disabled state across restarts |
http_client |
None |
bring your own httpx.Client / AsyncClient |
Provider knobs — OpenRouter(...), GoogleAIStudio(...), NIM(...):
| Param | Default | What it does |
|---|---|---|
keys |
— | one key (str) or many (list, or comma-string via env) |
tier |
"free" |
selects built-in rpm/rpd limits |
priority |
0 |
lower = tried first (tiebreak in every strategy) |
prefer |
[] |
model ids/substrings to move to the front of resolution |
free_only |
OpenRouter True, else False |
block paid model ids (see below) |
rpm / rpd |
tier default | override requests-per-minute / per-day |
models |
discovered / built-in | override model list (order = preference) |
discover |
OpenRouter True, else False |
live-fetch /models |
cache_ttl |
env / 1h | discovery cache lifetime |
| Strategy | Behaviour |
|---|---|
priority |
providers in ascending priority, then list order. Deterministic. |
round_robin |
rotate which provider goes first each call. Spreads load evenly. |
quota_aware |
rank by current headroom (rpm tokens bounded by daily quota); cooling/disabled keys score 0. Unlimited-quota providers rank high but deplete as used, so traffic still spreads. |
latency |
prefer the provider with the lowest observed average latency. |
Whatever the ranking, candidates are interleaved across providers — the best model of every provider is tried before any provider's 2nd model — so failover always reaches every provider, even when your first provider has dozens of throttled free models.
from freelm import FreeLLM, OpenRouter, GoogleAIStudio, NIM
llm = FreeLLM(
[
OpenRouter("sk-or-...", priority=0), # try first
GoogleAIStudio("AIza...", priority=1), # then this
NIM("nvapi-...", priority=2), # last resort
],
strategy="priority",
)Within a provider, model preference is the order of its models list:
from freelm import OpenRouter, ModelSpec
OpenRouter("sk-or-...", discover=False, models=[
ModelSpec("openai/gpt-oss-120b:free", ("chat", "large")),
ModelSpec("meta-llama/llama-3.3-70b-instruct:free", ("chat", "large")),
])freelm is free-only by default and by guard, not just by convention:
- OpenRouter mixes paid and free models in one catalog, so it ships with
free_only=True: passing a non-:freemodel id raisesConfigErrorinstead of silently billing you. Opt out per provider:OpenRouter(key, free_only=False). - Google AI Studio is free unless you pick
tier="tier1"(billing enabled). - NVIDIA NIM burns build.nvidia.com credits — free until they run out (requests then fail, not bill).
- Groq / Cerebras / Mistral free-tier accounts: every model is free at that tier.
tools, tool_choice, and response_format pass straight through to the
provider; chat:tools routes to models that support function calling:
r = llm.chat(msgs, model="chat:tools", tools=[...], tool_choice="auto")
r.tool_calls # [{"id": ..., "function": {...}}] or None
llm.chat(msgs, response_format={"type": "json_object"})(Tool calls are non-streaming for now; stream() yields text deltas only.)
Thinking-model gotcha: reasoning models (gemini-2.5-flash, gpt-oss, ...) can spend a small
max_tokensbudget entirely on hidden reasoning and return empty text withfinish_reason="length". Give them headroom (≥128) or pick a non-thinking model —autoalready deprioritizesreasoning-tagged models.
Watch every attempt, failover, and success without wrapping the client:
def hook(e): # freelm.Event
print(e.kind, e.provider, e.model, e.status, e.latency_ms)
llm = freelm.FreeLLM.from_env(on_event=hook)
# attempt openrouter openai/gpt-oss-20b:free None None
# error openrouter openai/gpt-oss-20b:free 429 None
# attempt google gemini-2.5-flash None None
# success google gemini-2.5-flash None 412.3kind is attempt | success | error | wait | discovery; keys are always
masked. A raising callback never breaks the call. llm.health() still gives
point-in-time state.
By default counters live in memory, so a restarted process re-burns exhausted keys. Opt in to disk persistence (shared schema with the JS package):
llm = freelm.FreeLLM.from_env(persist=True) # or env FREELM_PERSIST=1State (rpd_used, cooldowns, disabled keys — never raw keys, only hashes)
lives in ~/.cache/freelm/state.json (0600), loaded at construction and saved
after each call. Multi-process is last-writer-wins, best effort.
The package installs a freelm command (pipx install freelm, or npx freelm
for the JS package):
freelm chat "explain failover in one line" --model chat:fast --stream
freelm models --provider openrouter # live free-model list
freelm health # per-key readiness/quota tablefrom freelm import NoProvidersAvailable, ProviderError
try:
resp = llm.chat("hi")
except NoProvidersAvailable as e:
print("all providers exhausted:", e.attempts) # [(candidate, exception), ...]
except ProviderError as e:
print(e.provider, e.status, e.retryable) # e.g. a malformed 400Hierarchy: FreeLLMError → ConfigError · NoProvidersAvailable · ProviderError → AuthError / QuotaExhausted / RateLimited / Transient / ModelNotFound. Retryable errors (RateLimited, Transient) are handled internally and only surface, bundled, inside NoProvidersAvailable. AuthError (401/403) and QuotaExhausted (402, e.g. OpenRouter out of credits) disable the key and fail over instead of aborting the call.
r = llm.chat("hi")
r.text # assistant text (also: str(r))
r.provider # which provider served it, e.g. "openrouter"
r.model # concrete model id used
r.usage # .prompt_tokens / .completion_tokens / .total_tokens
r.latency_ms # round-trip latency
r.raw # original provider JSONllm.health() → one dict per key: provider, key (masked), ready, breaker, rpd_used, last_error, ewma_latency_ms.
Concurrency:
AsyncFreeLLMis safe across many concurrent tasks on one event loop. A syncFreeLLMmutates per-key state without locks — use one client per thread, or use the async client, for multi-threaded workloads.
- Key pool per provider, round-robined to spread load.
- Failover chain: interleaved across providers (best model of each, then next-best) so every provider is reached fast — never starved by one provider's many models.
- Circuit breaker per key: opens after repeated failures, half-opens after a cooldown — no hammering a dead key.
- Retry classification:
429→ cool the key & rotate;5xx/timeout → breaker + backoff;401/403/402→ disable the key;4xxmodel errors → try another model/provider; other4xx→ surfaced as a caller bug. - Quota guard: per-key requests/minute (token bucket) + requests/day counter, so a key predicted to be exhausted is skipped before you waste a call.
wait=True(optional): briefly sleep until a key frees up instead of failing, bounded bymax_wait.
Inspect live state any time:
for row in llm.health():
print(row) # provider, key (masked), ready, breaker, rpd_used, last_error, latencyShipped: streaming (0.2.0), JS/TS port (npm freelm), model/provider priority +
free-only guard + tool-calling passthrough + observability + CLI + persistent
quota state (0.3.0).
- next — token-based pacing (TPM/TPD budgets from response usage)
- then — streaming tool calls; deeper structured-output normalization
- later — embeddings, vision; Go port
freelm is a client-side, free-tier-only failover layer — not a proxy server, not an agent framework:
| Tool | What it is | How freelm differs |
|---|---|---|
| LiteLLM | SDK + proxy server for 100+ providers (paid & free) | freelm is free-only, zero-infrastructure (no proxy to run), with quota/breaker state per key built in |
| OpenRouter SDK | Client for one aggregator | OpenRouter is one of freelm's six pools — when its free quota dries up, freelm fails over to Gemini, Groq, Cerebras, Mistral, or NIM directly |
| LangChain / LlamaIndex | Orchestration frameworks | freelm is a thin client; use it inside them via the OpenAI-compatible shim |
Install freelm, set one or more free API keys (OpenRouter, Google AI Studio, NVIDIA NIM, Groq, Cerebras, or Mistral) as environment variables, and call freelm.FreeLLM.from_env().text("..."). freelm picks an available free model and handles rate limits and failover automatically.
npm install freelm (Node ≥ 18, zero runtime dependencies), set the same env keys, and call await FreeLLM.fromEnv().text("..."). The TypeScript package mirrors the Python API — same providers, same failover engine, same streaming.
Yes. Six providers ship usable free tiers in 2026 — OpenRouter (:free models), Google AI Studio, NVIDIA NIM, Groq, Cerebras, and Mistral — and freelm.compat.OpenAI is a drop-in for the OpenAI SDK that routes chat.completions.create(...) across all of them with automatic failover, including stream=True.
Verified 2026-06: OpenRouter (~50 req/day under $10 lifetime credit, ~1000/day at ≥$10), Google AI Studio (per-model free quotas), NVIDIA NIM (free against build.nvidia.com credits), Groq (30 RPM / 14,400 req/day, no card), Cerebras (~30 RPM, 1M tokens/day), Mistral (Experiment tier: 2 RPM, 1B tokens/month). freelm ships these as tier defaults you can override.
Pass several providers to FreeLLM([...]). On a rate limit (429), dead key (401), exhausted credits (402), or server error, freelm rotates keys and fails over to the next provider — interleaved so every provider is reached quickly instead of stalling on one.
Yes — from freelm.compat import OpenAI is a drop-in for the OpenAI SDK (client.chat.completions.create(...)), backed by free providers.
freelm paces each key with a requests-per-minute token bucket plus a daily counter and skips keys predicted to be exhausted. Add more keys or providers to raise total throughput.
Free model IDs change constantly, so freelm discovers them live from the provider API and caches them. Run from freelm import list_free_models; list_free_models() for the current list.
freelm itself is MIT-licensed and free. It runs on providers' free tiers; the actual request limits depend on each provider's free quota.
MIT © Shahriar Labs
Free-tier model lists change often —
freelmdiscovers OpenRouter models live and caches them, so you rarely touch the hardcoded list. Tier rate-limit numbers are still heuristic defaults; overriderpm/rpd/tieras providers evolve.