LiteLLM
Route Melious through the LiteLLM SDK or proxy — one config block, 100+ tools downstream
LiteLLM is an open-source AI gateway: a Python SDK and a proxy server that put one OpenAI-shaped interface in front of every provider, with retries, fallbacks, budgets, and spend tracking built in. It's the layer a lot of the ecosystem already sits on — Aider and OpenHands both talk to models through it — so adding Melious to a LiteLLM config usually means every tool behind that gateway reaches European infrastructure at once.
Two ways in, and you'll probably use both. The SDK is a drop-in completion() call for your own Python. The proxy is a long-lived server that speaks /v1/chat/completions and /v1/messages on localhost:4000, so anything that can point at an OpenAI or Anthropic base URL gets Melious without knowing we exist.
Setup
Install
pip install litellmpip install 'litellm[proxy]'Installs the litellm CLI alongside the SDK.
docker run -p 4000:4000 \
-v $(pwd)/config.yaml:/app/config.yaml \
-e MELIOUS_API_KEY=sk-mel-<YOUR_API_KEY> \
-e LITELLM_MASTER_KEY=sk-<YOUR_PROXY_KEY> \
ghcr.io/berriai/litellm:main-stable \
--config /app/config.yamlExport your key
export MELIOUS_API_KEY=sk-mel-<YOUR_API_KEY>Point LiteLLM at Melious
import os
from litellm import completion
response = completion(
model="openai/glm-5.1",
api_base="https://api.melious.ai/v1",
api_key=os.environ["MELIOUS_API_KEY"],
messages=[{"role": "user", "content": "Name three Hanseatic cities."}],
)
print(response.choices[0].message.content)The openai/ prefix selects LiteLLM's OpenAI-compatible transport, not OpenAI the company — api_base decides who answers.
model_list:
- model_name: glm-5.1
litellm_params:
model: openai/glm-5.1
api_base: https://api.melious.ai/v1
api_key: os.environ/MELIOUS_API_KEY
litellm_settings:
drop_params: trueos.environ/ tells LiteLLM to read the value at startup rather than storing the key in the file. drop_params silently discards parameters a model doesn't support instead of erroring — worth having on when clients send OpenAI-only fields.
Run and check
litellm --config config.yamlThe proxy listens on http://localhost:4000. Send it a request the way any OpenAI client would:
curl http://localhost:4000/v1/chat/completions \
-H "Authorization: Bearer sk-<YOUR_PROXY_KEY>" \
-H "Content-Type: application/json" \
-d '{
"model": "glm-5.1",
"messages": [{"role": "user", "content": "Name three Hanseatic cities."}]
}'Set LITELLM_MASTER_KEY before starting the proxy and use it as the bearer token — it's the proxy's own key, not your Melious one, which never leaves the config.
Model routing
model_name is the alias clients ask for; litellm_params.model is what we actually run. That indirection is the whole point of the proxy — swap the model underneath and no client changes.
One entry per model when you want a curated list:
model_list:
- model_name: fast
litellm_params:
model: openai/qwen3.5-9b
api_base: https://api.melious.ai/v1
api_key: os.environ/MELIOUS_API_KEY
- model_name: coding
litellm_params:
model: openai/qwen3-coder-next
api_base: https://api.melious.ai/v1
api_key: os.environ/MELIOUS_API_KEYA wildcard when you'd rather expose the whole catalog and let callers name the model:
model_list:
- model_name: melious/*
litellm_params:
model: openai/*
api_base: https://api.melious.ai/v1
api_key: os.environ/MELIOUS_API_KEYClients then ask for melious/glm-5.2, melious/kimi-k3, or anything else our GET /v1/models lists, with no config change per model. One wart: the proxy's own /v1/models expands openai/* from LiteLLM's built-in OpenAI catalog, so the list it advertises is full of melious/gpt-… names that don't exist here. Routing is unaffected — a real Melious model ID works whether or not the listing mentions it — but if a client populates its model picker from that endpoint, prefer explicit entries over the wildcard.
Routing flavors survive the trip. Append :eco, :price, :speed, or :balanced to bias which European provider serves the request — LiteLLM passes the model string through untouched, so openai/glm-5.1:eco reaches us as glm-5.1:eco. See Routing for what each flavor weights.
Fallbacks chain models when one errors out:
router_settings:
fallbacks: [{ "glm-5.1": ["qwen3.5-397b-a17b"] }]
num_retries: 2Listing two Melious entries under the same model_name also load-balances between them, which is mostly useful for splitting traffic across models rather than across providers — provider-level failover already happens on our side.
Streaming
Set stream=True and LiteLLM hands you an iterator of OpenAI-shaped chunks:
for chunk in completion(
model="openai/glm-5.1",
api_base="https://api.melious.ai/v1",
api_key=os.environ["MELIOUS_API_KEY"],
messages=[{"role": "user", "content": "Summarize GDPR in two sentences."}],
stream=True,
):
print(chunk.choices[0].delta.content or "", end="", flush=True)curl -N http://localhost:4000/v1/chat/completions \
-H "Authorization: Bearer sk-<YOUR_PROXY_KEY>" \
-H "Content-Type: application/json" \
-d '{
"model": "glm-5.1",
"messages": [{"role": "user", "content": "Summarize GDPR in two sentences."}],
"stream": true
}'Reasoning models stream their thinking as reasoning_content deltas before the answer arrives in content, so a naive loop that only prints content looks idle for the first few seconds. That's the model working, not a stalled stream.
The Anthropic-shape endpoint
Melious implements POST /v1/messages too, and LiteLLM can front it. Add a second entry using the anthropic/ transport against our root URL — no /v1, because the Anthropic path appends its own:
model_list:
- model_name: claude-sonnet-4
litellm_params:
model: anthropic/claude-sonnet-4
api_base: https://api.melious.ai
api_key: os.environ/MELIOUS_API_KEYClaude Code and anything else built on the Anthropic SDK can then use the proxy as its ANTHROPIC_BASE_URL. We map claude-* names onto open-weight models and echo the requested name back, which is what keeps those clients working unmodified — see From Anthropic for the mapping table. To run a specific model instead, name it directly: anthropic/glm-5.1.
Native provider on the way
A melious/ provider is landing upstream in LiteLLM, which removes the api_base line entirely — model: melious/glm-5.1 plus MELIOUS_API_KEY will be the whole config, on both the OpenAI and the Anthropic surface. Until that ships in a release, the configs above are the way.
Cost tracking
LiteLLM prices requests from its own model map, which doesn't know our rates yet, so declare them per deployment:
model_list:
- model_name: glm-5.1
litellm_params:
model: openai/glm-5.1
api_base: https://api.melious.ai/v1
api_key: os.environ/MELIOUS_API_KEY
model_info:
input_cost_per_token: 0.0000013
output_cost_per_token: 0.00000405Every response then comes back with an x-litellm-response-cost header, and /spend/logs aggregates per key and per model once you've given the proxy a DATABASE_URL. Current per-model rates are on the models hub and in _meta.pricing from GET /v1/models?include_meta=true. Ours are in EUR per million tokens; LiteLLM's fields are per single token, so divide by a million.
Melious' own accounting rides along independently. Every response carries environment_impact and billing_cost objects, and both survive the trip: response.environment_impact on an SDK call, and the same keys in the JSON body through the proxy. The usage dashboard aggregates them if you'd rather not read them per request.
What's different
- Two base URLs, one key. The OpenAI-compatible surface is
https://api.melious.ai/v1; the Anthropic one is the root,https://api.melious.ai. Mixing them up is the most common misconfiguration here. - No explicit cache control. There's no field to mark a prefix cacheable. Transparent prefix caching still happens on our side and comes back as
usage.cached_tokens, billed at the cheaper cache-read rate. See Models. GET /v1/modelson the proxy lists your aliases, not our catalog. It reports whatmodel_listexposes, and under a wildcard it reports LiteLLM's built-in list for the transport you borrowed. Query us directly to see everything we run.- Tool calling, JSON mode, and vision work through the standard OpenAI fields on models that support them. Filter
GET /v1/models?include_meta=trueby_meta.capabilitiesbefore assuming.
When it breaks
NotFoundError: Model not found: <id>— the ID after the transport prefix isn't one of ours.GET /v1/modelsfor the canonical list; remember the prefix isopenai/, and the part after it is the Melious model ID.NotFoundError: The requested resource was not found— the model is fine,api_baseis wrong for the surface. Chat completions needs the/v1suffix; the Anthropic path must not have it.AuthenticationErrorfrom the proxy, not from us — you sent your Melious key where the proxy wanted its own. Clients authenticate withLITELLM_MASTER_KEYor a generated virtual key; only the config holdsMELIOUS_API_KEY.- Empty
contentwithfinish_reason: "stop"— a reasoning model spent the wholemax_tokensbudget on its thinking pass before emitting an answer. It bites hardest withresponse_format, where you get{}or an empty string back. Raisemax_tokensor pick a non-reasoning model;_meta.reasoning_typeonGET /v1/models?include_meta=truetells you which is which. - Rate limit errors under a fan-out workload — LiteLLM will happily run more concurrency than your plan's per-minute token cap allows. Set
rpm/tpmon the deployment, or see Rate limits for which plan lifts which limit.
Errors and retry patterns: Errors.