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.

In short: LiteLLM routes any OpenAI-compatible endpoint if you prefix the model ID with openai/ and pass api_base. Its built-in cost table does not know a gateway's rates, so derive cost from returned token usage and your own rates instead.

What you need

Confirm the key works first. One command, no SDK, costs nothing:
Don't have an account yet? You need an API key from the gateway before the steps below will work — how the discount works and what you take on. Create one free → (no prepay, $1 minimum top-up)
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.

You need a key before the code below runs. Create an account, generate a key, and copy the base URL (https://aicomp.ai/v1). Create one free →
Check current rates → Free to sign up · $1 minimum top-up · No prepayment

How this fails in practice

SymptomCauseFix
LLM Provider NOT providedMissing openai/ prefixPrefix every model ID used with a custom base URL
404 on /chat/completionsTrailing path doubled in api_baseEnd the base URL at /v1
400 about an unsupported parameterVendor-specific fields forwarded to a model that lacks themdrop_params: true in proxy settings
Cost logs disagree with your invoiceBuilt-in cost map does not know gateway ratesCompute cost from returned token usage and your own rates
Timeout under loadNo retry or timeout setnum_retries and request_timeout in settings

Why changing one URL is enough

An OpenAI-compatible client does not have a provider hardcoded into it. It holds a base URL, and at request time it concatenates that base URL with a resource path — /chat/completions, /models, then sends the same JSON body it would have sent anywhere else, with your key in an Authorization: Bearer header. The base URL is therefore the only thing that decides where the request physically goes.

That is why this change is one line and not a rewrite. Your message array, your tool definitions, your streaming handling and your response parsing are all unchanged, because the wire format is unchanged. What changes is who answers, and what they charge per token. The two values that travel with the new destination are the base URL itself and the API key; the model ID is the third, and it has to be one the new endpoint actually serves, which you can list with a single call to /v1/models rather than guess at.

The practical consequence is that this is reversible. Keep the base URL in configuration rather than inline, and moving between providers is a config change you can undo in a minute — which is also the right way to test it: point a single non-critical call at the new endpoint first, confirm it in the usage log, then move the rest.

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:

Cost of one heavy coding day (200k input / 60k output tokens) and a 20-day month. Rates checked 2026-11-09.
ModelVendorRate
in / out per 1M
Per dayPer month
gpt-5.6-lunaOpenAI$0.1 / $0.6$0.06$1
deepseek-v4-flashDeepSeek$0.15 / $0.6$0.07$1
MiniMax-M3MiniMax$0.15 / $0.6$0.07$1
gemini-3.7-flashGoogle$0.375 / $1.875$0.19$4
claude-haiku-4-5-20251001Anthropic$0.5 / $2.5$0.25$5
deepseek-v4-proDeepSeek$0.66 / $1.98$0.25$5
glm-5.3Zhipu$0.7 / $2.2$0.27$5
qwen3.8-maxAlibaba$1 / $3$0.38$8
claude-sonnet-5Anthropic$1 / $5$0.50$10
gpt-5.6-terraOpenAI$1 / $6$0.56$11
kimi-k3Moonshot$1.5 / $7.5$0.75$15
claude-opus-5Anthropic$2.5 / $12.5$1.25$25
The same three models across three workload shapes, one month, gateway rates
Workloadgpt-5.6-lunaclaude-sonnet-5deepseek-v4-flash
Chat assistance
50 requests a day, 2k in / 500 out
$0$4$1
Bulk processing
5,000 short jobs a day, 800 in / 300 out
$26$230$30
Agent loop
20 turns a day, 60k in / 800 out
$3$26$4

One month at 20 working days, gateway rates checked 2026-11-09. These are the token volumes, not a quote Your own shape may be larger or smaller.

Worth reading both tables together: the agent-day table is one point on a curve, and this one shows that the curve does not rank models the same way everywhere. Where a workload is mostly input, the input rate decides it; where it is mostly output, the output rate does. A fallback chain that sends cheap traffic to one model and hard traffic to another is only saving money if the two models are actually cheaper on the shapes you are sending them.

Cost note. Routing by task beats routing by model reputation. A cheap model handling autocomplete and summaries while a stronger one handles refactors usually lands well below the monthly figure of running everything on the strongest row, and the split survives a price change better than any single-model commitment.

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.

What is the LiteLLM base URL for an OpenAI-compatible endpoint?

It goes in api_base — per call as completion(model="openai/…", api_base="…"), or once per model in the proxy config under litellm_params. Include the version prefix your gateway expects (most OpenAI-compatible routes end in /v1); it is set independently of api_key, so a working key with a missing or wrong base URL still fails.

How do I set api_base in the LiteLLM proxy config.yaml?

Put it under litellm_params for each entry in model_list. That is the only place the proxy reads it from. A provider environment variable in the proxy process works as a fallback, but an explicit model_list entry wins and is far easier to audit once several models share one endpoint.

Does LiteLLM work with any OpenAI-compatible provider?

Yes. It routes on the model prefix rather than the vendor name, so anything speaking the OpenAI /chat/completions shape works — you only need to keep the openai/ prefix so LiteLLM knows which request format to build. One caveat: models your gateway exposes that LiteLLM has never seen need their rate added by hand, or its cost tracking will quietly report the wrong number.

Related

Get API access

Rate cards: GPT, DeepSeek and Claude