LlamaIndex with a custom endpoint

LlamaIndex splits routing across two independent objects (the LLM and the embedding model) and most people only change the first one. That is why a migration that looks finished leaves half the traffic on the old provider, and half the bill on the old account.

In short: LlamaIndex's OpenAI class validates model names against OpenAI's own list and rejects gateway-served IDs, so use OpenAILike with is_chat_model=True. The embedding model is a separate object, setting only Settings.llm leaves embeddings on the default provider.
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
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.

What you need before you start

Both objects, in one place

Since v0.10 the model classes live in separate packages, and the OpenAI-compatible variants carry the -like suffix. The distinction matters: the plain OpenAI class validates the model name against OpenAI's own list, so a gateway-served ID is rejected before any request is made.

import os
from llama_index.core import Settings
from llama_index.llms.openai_like import OpenAILike
from llama_index.embeddings.openai_like import OpenAILikeEmbedding

# OpenAILike accepts any model string. The plain OpenAI class validates the
# name against OpenAI's own list and rejects gateway-served IDs.
Settings.llm = OpenAILike(
    model="gpt-5.6-luna",
    api_key=os.environ["GATEWAY_API_KEY"],
    api_base="https://aicomp.ai/v1",
    is_chat_model=True,          # use /chat/completions, not /completions
    temperature=0.1,
    max_tokens=1024,
)

# The embedding model is a SEPARATE object. Leaving it unset sends embeddings
# to whatever the default is -- usually straight to OpenAI, on a second bill.
Settings.embed_model = OpenAILikeEmbedding(
    model_name="text-embedding-3-small",
    api_key=os.environ["GATEWAY_API_KEY"],
    api_base="https://aicomp.ai/v1",
    embed_batch_size=64,
)

Two lines in that block are the ones people omit. is_chat_model=True selects /chat/completions over the legacy completions route. Without it you get a request the endpoint was never going to answer well. And Settings.embed_model is the one that decides where your indexing bill goes.

The cost shape nobody budgets for

Indexing and querying are not the same expense wearing different hats. Indexing is one large embedding bill paid once per rebuild; querying is one small embedding call plus one generation call, paid per question. A pipeline that looks cheap per query can be dominated by re-indexing if the document set churns.

from llama_index.core.callbacks import CallbackManager, TokenCountingHandler
from llama_index.core import Settings

counter = TokenCountingHandler()
Settings.callback_manager = CallbackManager([counter])

# ... build the index, run queries ...

# Indexing and querying have completely different cost shapes:
#   indexing = one large embedding bill, paid once per rebuild
#   querying = one small embedding call + one generation call, paid per question
print("embedding tokens:", counter.total_embedding_token_count)
print("prompt tokens   :", counter.prompt_llm_token_count)
print("completion tok. :", counter.completion_llm_token_count)
print("resets:", counter.reset_counts())

Separating the two counters is the whole point. If the embedding token count is large relative to the prompt count, your lever is chunking and rebuild frequency, not model choice. Swapping to a cheaper generation model barely moves a bill that is mostly embeddings.

If you serve more than one tenant

Settings is global mutable state. Two concurrent requests that each assign Settings.llm will read each other's model, and the failure is intermittent enough to survive code review.

from llama_index.core import Settings, VectorStoreIndex

def build_and_query(docs, question, llm, embed_model):
    # Per-request scope instead of mutating the global Settings.
    return Settings.withLLM(llm, lambda: Settings.withEmbedModel(
        embed_model,
        lambda: VectorStoreIndex.from_documents(docs).as_query_engine().query(question),
    ))

# Global Settings is mutable process state. Two concurrent requests that each
# assign Settings.llm will read each other's model.

What it costs

Generation models reachable from LlamaIndex — gateway rate vs official list
ModelGateway rate
in / out per 1M tokens
Official list
in / out per 1M tokens
Diff
gpt-5.6-luna$0.1 / $0.6$0.2 / $1.250%
claude-haiku-4-5$0.5 / $2.5$1 / $550%
deepseek-v4-flash$0.15 / $0.6— / ——

Rates checked 2026-11-14. Gateway rates move with upstream promotions (verify the current number in your dashboard before committing to a budget).

Rates checked 2026-11-14. Embedding models are billed separately from generation and are usually priced per token at a fraction of generation rates, which is exactly why they are easy to overlook until the index is rebuilt.

LlamaIndex failures and what each one is actually telling you

SymptomWhat is actually happeningHow to confirmFix
ValueError on a model ID the endpoint servesThe plain OpenAI class validates the name against OpenAI's own model list and rejects unknown IDs.The error arrives before any HTTP request (no entry in the usage log).Switch to OpenAILike and pass is_chat_model=True.
Half the traffic still goes to OpenAISettings.embed_model was never set, so embeddings use the default route.Compare the embedding call count in one dashboard against the generation count in the other.Set both objects and re-verify with a small index build.
Dimension mismatch at query timeThe index was built with a different embedding model than the one querying it.Compare the stored vector dimension against the model's output dimension.Rebuild the index, there is no in-place conversion.
Intermittent wrong-model answers under concurrencyGlobal Settings mutated per request; concurrent requests overwrite each other.Log the resolved model inside the request handler, not at startup.Use Settings.withLLM / withEmbedModel to scope the override.
Requests hit /completions instead of /chat/completionsis_chat_model defaults to False on OpenAILike.Check the path in the endpoint's request log.Pass is_chat_model=True explicitly.
A rebuild costs far more than expectedIndexing embeds every chunk; the cost scales with document volume, not with query volume.Read the embedding token counter separately from the generation counter.Tune chunk size and rebuild frequency before changing models.

When this is the wrong move

Cases where routing LlamaIndex through a gateway is the wrong move

SituationWhy it breaksDo this instead
Your index is already built with a different embedding modelSwitching providers means re-embedding every document. The one-off cost can exceed months of query savings.Price the rebuild against the projected query savings before migrating.
You serve concurrent tenants from one processGlobal Settings is mutable process state and does not isolate per request.Use the withLLM / withEmbedModel scoping helpers.
You only need generation, not retrievalLlamaIndex's value is the retrieval layer. A plain SDK call carries less configuration surface and fewer places to get half-migrated.Use the OpenAI SDK directly (see the SDK guide).

How to confirm the change actually took effect

"I set it and nothing happened" is the most common failure here, and it is almost never the endpoint. It is usually the client: a desktop app that reads its own settings file rather than your shell environment, a process started before you exported the variable, or an SDK client instance built once at import time and reused after the config changed. Verify before you debug anything else.

Layer 1, is the endpoint reachable, and does your key work?

curl -s https://aicomp.ai/v1/models \
  -H "Authorization: Bearer $KEY" | head -c 400

A JSON list of model IDs means both are true. An Invalid token body means the endpoint answered but rejected the credential The URL is right, the key is not. An HTML page means something between you and the endpoint (a corporate proxy, a captive portal) answered instead, and no amount of config change will fix it from inside your app.

Layer 2 — who actually answered?

from openai import OpenAI
client = OpenAI(base_url="https://aicomp.ai/v1", api_key=KEY)

r = client.chat.completions.create(
    model="gpt-5.6-luna",
    messages=[{"role": "user", "content": "Reply with the single word: ok"}],
    max_tokens=8,
)
print(r.model)          # 回显的 model 是谁 —— 这是端点身份的唯一硬证据
print(r.usage)          # prompt_tokens / completion_tokens

The model field echoed back is the only hard evidence of identity. A request that silently fell through to a default provider still returns HTTP 200 and still returns sensible text, which is exactly why this layer exists. If the echoed model is not the one you asked for, you are talking to someone else.

Layer 3 — did it land in the usage log?

Reachability is not billing. Open the provider's usage view and look for the model ID you just called, on today's date. If the call succeeded but does not appear, you either hit a cached response or you are still on the old path. This layer is also the one that catches the case where two environments share a key and you are reading the wrong one.

Three ways this check lies to you

FAQ

Why does LlamaIndex reject a model ID the endpoint serves?

The OpenAI and OpenAIEmbedding classes check the model name against OpenAI's own model list and refuse anything they do not recognise. That check is the reason a gateway-served ID fails before a single request leaves your process. The fix is to use OpenAILike from llama-index-llms-openai-like, which accepts any model string and sends exactly the same requests. Pass is_chat_model=True so it targets /chat/completions rather than the legacy completions route.

Why is my bill in two places after I switched?

Because the embedding model is a separate object from the LLM. Setting Settings.llm routes generation and leaves Settings.embed_model at its default, which is typically a direct OpenAI connection. Indexing then bills to one account and querying to another, and the costs do not appear together in either dashboard. Set both, and verify by building a small index and checking that both an embedding call and a generation call show up under the same key.

Is Settings safe to mutate in a multi-tenant service?

No. Settings is global mutable process state, and two concurrent requests that each assign Settings.llm will read each other's model. The documented pattern is Settings.withLLM(...) and Settings.withEmbedModel(...), which scope the override to a callback rather than mutating the global. The snippet above shows the shape.

Which package names do I need since v0.10?

The model classes were split out of core: llama-index-llms-openai and llama-index-embeddings-openai for OpenAI itself, and the -like variants for any other OpenAI-compatible endpoint. A bare pip install llama-index is no longer sufficient to get the classes the code on this page imports.

Do I need to rebuild my index after switching embedding models?

Yes, if the vector dimensionality or the model changes. An index built with a 1536-dimension embedding model cannot be queried with a 3072-dimension one, the mismatch surfaces at query time, not at build time. This is the expensive version of the switch: every document gets re-embedded, which is the single largest one-off cost in most RAG pipelines.

Get API access

Rate cards: GPT and Llama

Related