LiteLLM with a custom OpenAI-compatible endpoint
LiteLLM exists to stop you writing vendor-specific code. Point it at one OpenAI-compatible base URL and the same completion() call reaches every model your gateway exposes — then add a fallback so a provider outage becomes a footnote rather than an incident.
What you need
- A gateway API key.
- The base URL:
https://aicomp.ai/v1. - At least one exact model ID from
GET /models— LiteLLM does not guess these.
curl https://aicomp.ai/v1/models \
-H "Authorization: Bearer sk-your-gateway-key"
A JSON list of model IDs means the key is good. Invalid token means it was copied wrong.
Step 1 — the smallest working call
Read this one carefully, because the prefix is where most people lose twenty minutes:
import os
from litellm import completion
# LiteLLM needs the provider prefix to know which wire format to speak.
# Everything OpenAI-compatible uses "openai/", including when you point it
# at a gateway rather than at OpenAI.
resp = completion(
model="openai/deepseek-v4-pro",
api_base=os.environ["GATEWAY_BASE_URL"], # https://aicomp.ai/v1
api_key=os.environ["GATEWAY_API_KEY"],
messages=[{"role": "user", "content": "Summarise this file."}],
max_tokens=400,
)
print(resp.choices[0].message.content)
The openai/ prefix selects the request format, not the vendor. With api_base set, it is the format that matters — the request goes where you told it to go.
Step 2 — streaming (what every chat UI needs)
from litellm import completion
resp = completion(
model="openai/deepseek-v4-pro",
api_base=os.environ["GATEWAY_BASE_URL"],
api_key=os.environ["GATEWAY_API_KEY"],
messages=[{"role": "user", "content": "Write the migration SQL."}],
stream=True,
)
for chunk in resp:
delta = chunk.choices[0].delta.content
if delta:
print(delta, end="", flush=True)
Step 3 — add a fallback before you need one
Fallbacks are cheap insurance and they compound well with mixed pricing: put an inexpensive model first and promote to a stronger one only when the cheap path fails.
from litellm import completion
# Cheap model first; if it errors, retry on the stronger one.
# The caller never sees which one answered.
resp = completion(
model="openai/deepseek-v4-flash",
api_base=os.environ["GATEWAY_BASE_URL"],
api_key=os.environ["GATEWAY_API_KEY"],
messages=[{"role": "user", "content": "Explain this stack trace."}],
fallbacks=[{"deepseek-v4-flash": ["deepseek-v4-pro"]}],
num_retries=2,
)
Step 4 — move it into the proxy when more than one process needs it
Once several services share the endpoint, repeating api_base per call is how drift starts. The proxy takes the same values once:
model_list:
- model_name: default
litellm_params:
model: openai/deepseek-v4-pro
api_base: https://aicomp.ai/v1
api_key: os.environ/GATEWAY_API_KEY
- model_name: cheap
litellm_params:
model: openai/deepseek-v4-flash
api_base: https://aicomp.ai/v1
api_key: os.environ/GATEWAY_API_KEY
litellm_settings:
drop_params: true # ignore params the upstream model does not accept
num_retries: 2
request_timeout: 60
# Routes: callers ask for "default", the proxy decides what that means today.
router_settings:
fallbacks: [{ default: ["cheap"] }]
Start it with litellm --config config.yaml --port 4000 and callers use one stable URL. Changing which model answers becomes a config edit instead of a deploy.
https://aicomp.ai/v1).
Create one free →
How this fails in practice
| Symptom | Cause | Fix |
|---|---|---|
LLM Provider NOT provided | Missing openai/ prefix | Prefix every model ID used with a custom base URL |
404 on /chat/completions | Trailing path doubled in api_base | End the base URL at /v1 |
| 400 about an unsupported parameter | Vendor-specific fields forwarded to a model that lacks them | drop_params: true in proxy settings |
| Cost logs disagree with your invoice | Built-in cost map does not know gateway rates | Compute cost from returned token usage and your own rates |
| Timeout under load | No retry or timeout set | num_retries and request_timeout in settings |
What a day of this actually costs
Fallback strategies look free until you read the invoice. Below is one heavy agent day — 200k input, 60k output — which is the shape of work these tools generate:
| Model | Vendor | Rate in / out per 1M | Per day | Per month |
|---|---|---|---|---|
| gpt-5.6-luna | OpenAI | $0.1 / $0.6 | $0.06 | $1 |
| MiniMax-M3 | MiniMax | $0.15 / $0.6 | $0.07 | $1 |
| deepseek-v4-flash | DeepSeek | $0.22 / $0.66 | $0.08 | $2 |
| gemini-3.7-flash | $0.375 / $1.875 | $0.19 | $4 | |
| claude-haiku-4-5-20251001 | Anthropic | $0.5 / $2.5 | $0.25 | $5 |
| deepseek-v4-pro | DeepSeek | $0.66 / $1.98 | $0.25 | $5 |
| glm-5.3 | Zhipu | $0.7 / $2.2 | $0.27 | $5 |
| qwen3.8-max | Alibaba | $1 / $3 | $0.38 | $8 |
| claude-sonnet-5 | Anthropic | $1 / $5 | $0.50 | $10 |
| gpt-5.6-terra | OpenAI | $1 / $6 | $0.56 | $11 |
| kimi-k3 | Moonshot | $1.5 / $7.5 | $0.75 | $15 |
| claude-opus-5 | Anthropic | $2.5 / $12.5 | $1.25 | $25 |
FAQ
Why does LiteLLM reject my model name without the openai/ prefix?
The prefix is how LiteLLM picks the request format and response parser. With a custom base URL and no prefix, it has nothing to match against and raises a mapping error. Every OpenAI-compatible route uses openai/.
Can I set api_base once instead of on every call?
Yes — set it per entry in the proxy model_list under litellm_params, or set the corresponding provider environment variable in the process that calls LiteLLM. Per-call arguments win over both, which is useful when two departments need different endpoints.
Does LiteLLM still track cost correctly with a custom endpoint?
It tracks what it knows. Its built-in cost map is compiled from public list prices and will not know a gateway's rate for a model it has never seen, so budget numbers can be wrong by a wide margin. Log usage.prompt_tokens and usage.completion_tokens yourself and multiply by your own rates — see per-request spend attribution.
What does drop_params actually change?
It stops LiteLLM forwarding parameters the upstream model rejects. Without it, sending a vendor-specific field to a model that does not implement it turns a working call into a 400. Set it when you route the same request shape across several vendors.
Is the proxy required?
No. It matters once more than one process — or more than one language — needs the same endpoint and key. Until then, the plain Python call above is enough and one moving part fewer.