Note : Below is a AI generated human reviewed bug report for a human discovered bug, I encountered the bug and had to fix it, I asked IA to document it.
If the quality is bad feel free to say so (I'll learn), or reject.
I use the fix, it is proven to work, the code changes are sound and reviewed. We can discuss about the asserts in the code (and remove them) but the bug is real and the fix make the code work according to the documentation.
I open the pull request right after pushing the issue.
Symptom
Using the webui LLM dialog (/api/v1/rag) with a litellm-typed config:
llm:
type: litellm
model: "openrouter/moonshotai/kimi-k3"
api_key: "sk-or-..."
the request went to api.openai.com with the OpenRouter key and failed:
openai.AuthenticationError: Error code: 401 - Incorrect API key provided ...
at serviette/server/main.py:243 -> llm.complete(...)
at serviette/server/llm.py:122 -> client.chat.completions.create(...)
LiteLLM was never initialized on this path.
Root cause
build_llm in packages/serviette/serviette/server/llm.py treated litellm
as OpenAI-compatible:
_OPENAI_COMPATIBLE = {"openai", "litellm"}
def build_llm(config):
...
if config.type in _OPENAI_COMPATIBLE:
return OpenAIChat(config)
OpenAIChat._ensure_client builds AsyncOpenAI(api_key=..., **extra). With
this config extra is empty, so base_url defaults to
https://api.openai.com. The provider prefix in model
(openrouter/...) was passed verbatim to OpenAI, which knows neither the
model nor the key → 401.
The config itself is valid — the quickstart wizard
(quickstart/wizard.py:607) offers litellm as an LLM type, so this was a
server-side misrouting, not a config error.
A second, latent instance of the same bug existed in
server/reranker.py: LLMReranker._ensure_chat constructed OpenAIChat
directly, ignoring a litellm type inherited from the top-level llm
section.
Fix
server/llm.py — added a LiteLLMChat backend that calls
litellm.acompletion(model=..., api_key=..., messages=...), so the
provider prefix in model selects the endpoint (OpenRouter here) and the
key is forwarded to that provider. build_llm now dispatches:
mock → MockLLM, litellm → LiteLLMChat, openai → OpenAIChat.
The openai path is unchanged.
server/reranker.py — _ensure_chat now uses build_llm(...)
instead of hardcoding OpenAIChat, so an LLM reranker honors the
configured type.
tests/test_server.py — regression test
test_litellm_routes_through_litellm_not_openai_client: drives
/api/v1/rag with a litellm LLM, patches litellm.acompletion, and
asserts the outgoing call carries the provider-prefixed model and the
API key, with no base_url (i.e. no OpenAI client involved).
Verification
- 43 tests pass (
test_server.py, test_rag_quality.py,
test_frontend.py, test_qdrant_hybrid.py), including the new
regression test.
- Manual check:
build_llm(LLMConfig(type="litellm", ...)) returns
LiteLLMChat; mocked litellm.acompletion receives
model="openrouter/moonshotai/kimi-k3" and the configured api_key.
Note: embedder.py intentionally keeps its own
_OPENAI_COMPATIBLE = {"openai", "litellm"} — server-side embedders
genuinely map litellm onto the OpenAI-compatible async client; that path
is unrelated and untouched.
Note : Below is a AI generated human reviewed bug report for a human discovered bug, I encountered the bug and had to fix it, I asked IA to document it.
If the quality is bad feel free to say so (I'll learn), or reject.
I use the fix, it is proven to work, the code changes are sound and reviewed. We can discuss about the asserts in the code (and remove them) but the bug is real and the fix make the code work according to the documentation.
I open the pull request right after pushing the issue.
Symptom
Using the webui LLM dialog (
/api/v1/rag) with alitellm-typed config:the request went to api.openai.com with the OpenRouter key and failed:
LiteLLM was never initialized on this path.
Root cause
build_llminpackages/serviette/serviette/server/llm.pytreatedlitellmas OpenAI-compatible:
OpenAIChat._ensure_clientbuildsAsyncOpenAI(api_key=..., **extra). Withthis config
extrais empty, sobase_urldefaults tohttps://api.openai.com. The provider prefix inmodel(
openrouter/...) was passed verbatim to OpenAI, which knows neither themodel nor the key → 401.
The config itself is valid — the quickstart wizard
(
quickstart/wizard.py:607) offerslitellmas an LLM type, so this was aserver-side misrouting, not a config error.
A second, latent instance of the same bug existed in
server/reranker.py:LLMReranker._ensure_chatconstructedOpenAIChatdirectly, ignoring a
litellmtype inherited from the top-levelllmsection.
Fix
server/llm.py— added aLiteLLMChatbackend that callslitellm.acompletion(model=..., api_key=..., messages=...), so theprovider prefix in
modelselects the endpoint (OpenRouter here) and thekey is forwarded to that provider.
build_llmnow dispatches:mock→MockLLM,litellm→LiteLLMChat,openai→OpenAIChat.The
openaipath is unchanged.server/reranker.py—_ensure_chatnow usesbuild_llm(...)instead of hardcoding
OpenAIChat, so an LLM reranker honors theconfigured type.
tests/test_server.py— regression testtest_litellm_routes_through_litellm_not_openai_client: drives/api/v1/ragwith alitellmLLM, patcheslitellm.acompletion, andasserts the outgoing call carries the provider-prefixed model and the
API key, with no
base_url(i.e. no OpenAI client involved).Verification
test_server.py,test_rag_quality.py,test_frontend.py,test_qdrant_hybrid.py), including the newregression test.
build_llm(LLMConfig(type="litellm", ...))returnsLiteLLMChat; mockedlitellm.acompletionreceivesmodel="openrouter/moonshotai/kimi-k3"and the configuredapi_key.Note:
embedder.pyintentionally keeps its own_OPENAI_COMPATIBLE = {"openai", "litellm"}— server-side embeddersgenuinely map
litellmonto the OpenAI-compatible async client; that pathis unrelated and untouched.