Skip to content

refactor(providers): unify provider management on litellm - #249

Merged
0xKT merged 25 commits into
mainfrom
refactor/provider_litellm_unification
Jul 30, 2026
Merged

refactor(providers): unify provider management on litellm#249
0xKT merged 25 commits into
mainfrom
refactor/provider_litellm_unification

Conversation

@0xKT

@0xKT 0xKT commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

Provider management used to be a whitelist: a registry of 21 vendors that
decided who could be configured at all, plus a second execution path that
bypassed LiteLLM entirely for knn routing. This makes LiteLLM the single
execution layer and reduces the registry to what LiteLLM cannot tell us.

knn routing now runs on LiteLLM. It needed several endpoints alive in one
process, which CustomProvider provided by talking to the endpoints directly --
at the cost of native streaming, retries and tool-calling. PerModelProvider
now builds one LiteLLMProvider per endpoint instead, and custom_provider.py
is deleted. knn is a decision layer again; execution is LiteLLM everywhere.

Any vendor LiteLLM supports is now configurable. A key under the vendor's
name plus a <vendor>/<model> id is enough -- 134 vendors instead of 21, with
no registry entry required. provider list, raven status, the startup gate
and the TUI model picker all report them, which they previously could not: a
working setup read as unconfigured, so Raven kept pushing the user into a wizard
that then refused to configure that vendor. Names LiteLLM has never heard of are
still rejected as typos, and a typo no longer makes Raven unstartable.

A provider now has one name. It used to have two -- ours and LiteLLM's --
reconciled by a litellm_prefix field read at every site that compared a name
or a model-id prefix. That field conflated two opposite facts: LiteLLM spelling
the same vendor differently (vllm -> hosted_vllm) versus a provider reached
through another vendor's driver (SiliconFlow over OpenAI's API). "Is this prefix
mine?" has opposite answers for the two, and deriving the distinction from
whether the prefix happened to be another entry's name was correct only by
coincidence. LiteLLM's spelling is now the provider's own name, litellm_prefix
is gone, and only a borrowed driver states via_driver -- which is absent from
route_names by construction, so an id asking for openai/ can no longer be
answered with SiliconFlow's key. zhipu is renamed to zai on the same basis.

Four facts were each implemented at several call sites and had already drifted
apart -- how a model id splits into a prefix, how a name is spelled for
comparison, which names refer to a provider, and how a provider's config
section is found. Each now has exactly one implementation, in registry.py or
ProvidersConfig.get.

Credential routing

Several paths could hand one vendor's key to another. All are fixed here:

  • a gateway's model prefix was read from the raw field rather than the derived
    one, so a gateway whose name is already LiteLLM's declared nothing and the
    prefix was dropped -- sending the gateway's key to the vendor named in the
    model id;
  • an explicit prefix could be overridden by a keyword match, so
    deepinfra/deepseek-ai/DeepSeek-V3 put DeepInfra's key in DEEPSEEK_API_KEY
    and rewrote the model id;
  • a bare id naming an unconfigured vendor fell back to any credentialed direct
    vendor, e.g. a lone Anthropic key answering for kimi-k2.5;
  • environment variables were derived from LiteLLM's validate_environment, so
    the key was written into every variable it listed --
    AWS_SECRET_ACCESS_KEY for Bedrock, CLOUDFLARE_API_BASE for Cloudflare;
  • litellm.api_base was set process-wide, leaking one provider's endpoint to
    every other call in the process;
  • the OpenRouter shortlist offered bare anthropic/... ids, which
    auto-detection reads as a request for that vendor direct, quietly leaving
    OpenRouter as soon as the user also held that vendor's key;
  • a routing.models entry without an api_base fell through to
    OPENAI_BASE_URL or api.openai.com, shipping the prompt and that
    endpoint's key to a third party; it now raises.

There is no attacker model here -- these are misrouting bugs, not an
authentication bypass -- but each one sends a credential somewhere its owner did
not intend, so they are called out rather than folded into the summary.

Also fixed

  • an empty declared section no longer answers for a provider, so credentials
    written under one of its other names are not shadowed by a placeholder;
  • differently-spelled sections (azure-openai, OpenRouter, nanoGpt) resolve
    to the same provider on both the read and the write path, so a section written
    as LiteLLM spells it is no longer invisible to provider get/set and a write
    no longer leaves two sections for one provider;
  • _litellm_knows returns three states instead of raising, because only one of
    its five call sites caught the exception;
  • the onboard wizard no longer drops api_key / api_base for vendors with no
    registry entry, and no longer overwrites a real key with None when a
    re-configure is abandoned;
  • raven status reads credentials from the loaded config, so ones supplied by
    environment variable are visible, and an OAuth provider without a token
    reports "not set" rather than a checkmark.

New

agents.defaults.modelOverrides lets a user override request params per model,
which previously only the registry could do. knn-routed models inherit it, so a
routed model behaves like the default provider.

Type

  • Fix
  • Feature
  • Docs
  • CI / tooling
  • Refactor
  • Other

Verification

uv run --all-extras pytest -q          # 4869 passed, 30 skipped, 0 failed
uv run ruff check raven/ tests/ benchmarks/
uv run ruff format --check raven/ tests/ benchmarks/

79 new tests across five files. The provider-resolution suite is sweep-shaped:
each invariant runs over every registered provider rather than the case that was
last fixed, since the drift this PR removes was invisible to case-shaped tests.
Every assertion was checked by breaking the implementation and confirming it
goes red.

It also carries five source guards against a second implementation reappearing.
These are line scans -- tripwires, not proofs. A deliberate rewrite walks past
them; what they catch is the shape that actually recurred, the same spelling
copied to a new call site.

Behaviour was compared against the base branch over a matrix of provider
configurations, model-id shapes and forced-provider settings. Every difference
falls into one of the intended changes below.

  • Relevant tests pass locally
  • Relevant lint / type checks pass locally
  • User-facing docs or screenshots are updated when needed

Risk

Three user-visible behaviour changes:

  1. A user whose only credentials are direct vendors -- no gateway -- and whose
    default model carries another vendor's prefix now gets "provider not
    configured" instead of a downstream 401. Anyone holding a gateway key is
    unaffected: a gateway legitimately routes any id.
  2. The canonical section names for vLLM and Ollama become hosted_vllm and
    ollama_chat, LiteLLM's own spellings. Existing configs keep loading under
    the old names; the next provider set consolidates onto the new one.
  3. A routing.models entry without an api_base now raises instead of
    silently defaulting to api.openai.com.

Rollback is a revert: no data migration runs, and configs written before this
change continue to load unchanged.

  • Security impact considered
  • Backward compatibility considered
  • Rollback path is clear for risky changes

Related Issues

Fixes #242
Fixes #243
Fixes #244
Fixes #245
Fixes #246

@0xKT
0xKT requested review from arelchan and ypflll July 30, 2026 13:32
0xKT and others added 25 commits July 30, 2026 22:07
The benchmark runners and the knn integration test each built a
CustomProvider to reach an OpenAI-compatible endpoint. Route them through
LiteLLMProvider with the generic `custom` gateway spec instead, so they
gain streaming, retry and usage normalization.

Extract the x-session-affinity header CustomProvider set unconditionally
into session_affinity_headers(), keeping cache locality on self-hosted
backends. The two pinchbench branches were identical once migrated, so
they collapse into one call.

Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
PerModelProvider dispatched each routable model to a CustomProvider that
talked to the openai SDK directly, so routed models had no native
chat_stream (they degraded to one delta), no retry ladder and no
usage/reasoning normalization. Build a LiteLLMProvider per endpoint
instead: knn keeps only the scheduling decision and litellm becomes the
single execution path. CustomProvider had no remaining consumer, so drop
it.

LiteLLMProvider no longer pins litellm.api_base on construction. Every
call already passes api_base as a kwarg, so the assignment only leaked
one provider's endpoint into the process-wide default that other callers
read when they omit it.

Drop CustomProvider from the backend-class catalog and cover the new
wiring: sub-providers are LiteLLMProvider, construction leaves the global
base unset, each endpoint sends its own base/key, and an already-prefixed
model name survives the gateway prefix.

Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
ModelEndpoint.api_base defaults to an empty string, and an empty base is
falsy, so the value never reached litellm as a kwarg. Litellm then
resolved the endpoint from OPENAI_BASE_URL or api.openai.com, meaning a
routing entry with a missing base silently shipped the prompt and that
entry's key to a third party. The former direct client raised a local
connection error for the same input, so nothing ever worked with an empty
base -- this only closes a state that was already broken, now loudly.

Guard the benchmark runner the same way: its default base is empty when no
RAVEN_BENCH endpoint is exported.

Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
A provider needs a ProviderSpec for its env keys, prefixes and detection
plus a ProvidersConfig field for its credentials. The two lists were kept
aligned by hand, and dropping either half leaves the provider
unconfigurable or unreachable without failing anything else.

Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
Zhipu, DashScope and Groq each carry a curated model shortlist and a
default_model, and test_provider_catalog treats them as seeded picker
providers, but the wizard's own list never listed them: interactively the
only ways in were --provider or picking "Other". Add the three and pin the
two lists together so they cannot drift apart again.

Drop the is_oauth copy from the curated entries. Every reader already
resolves it from the ProviderSpec, so the key was dead data waiting to go
stale.

Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
Kimi K2.5 rejects the default temperature, so the registry carried a
hard-coded override for it. That fixed one model in one provider and left
users with no way to do the same for any other model that needs a
different parameter.

Add agents.defaults.modelOverrides, keyed by a substring of the model
name. Config entries win; the registry's built-ins stay as defaults, so
Kimi keeps working with no config at all.

Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
Auto-detection matches a model against its own vendor first and only falls
back to a gateway when that vendor has no key. Users who deliberately route
everything through one gateway, for a single bill or a warm prompt cache,
had to spell the gateway prefix into every model name.

agents.defaults.gateway names a provider to claim bare model names. It sits
between the explicit-prefix pass and vendor matching, so it stays a soft
preference: "<provider>/<model>" still goes direct, and `provider` remains
the hard binding.

Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
LiteLLM speaks to well over a hundred vendors; Raven declared 21 and
rejected the rest, so reaching a supported vendor meant adding a
ProviderSpec and a schema field first.

ProvidersConfig now keeps unknown keys and serves them through get(),
which resolves declared fields and extras alike. Auto-detection honours an
explicit "<vendor>/<model>" prefix that names one of those extras, and the
env bridge asks LiteLLM which variable the vendor reads instead of relying
on a spec. A key under the vendor's name is now enough.

Read openrouter and groq through the same accessor: named attributes would
miss a provider that lives in the extras.

Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
OpenRouter model ids name the upstream vendor -- "anthropic/claude-opus-4.8",
"deepseek/deepseek-v4-flash". The shortlist stored them bare while this
provider's own default_model carried the "openrouter/" prefix, and
auto-detection reads a leading "anthropic/" as a request for Anthropic
direct.

So a model picked from the OpenRouter list left OpenRouter for the upstream
vendor as soon as the user also held that vendor's key: another bill,
another endpoint, no warning. The onboard wizard makes this reachable
because it persists the model without pinning agents.defaults.provider.

Prefix the shortlist to match default_model, and assert every entry
resolves to openrouter with a competing vendor key configured.

Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
The vendor rebranded to Z.ai and LiteLLM calls it "zai" -- it has no
"zhipu" provider at all. Raven kept the old brand as its config key while
every model id already read "zai/glm-4.6", so the provider name was the one
piece left behind, and it forced the spec to carry a name-to-prefix mapping
that nothing else needed.

Configs written as "zhipu" still load: the field validates under either key.

Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
Seven specs spelled out an env var, a model prefix and an endpoint that
LiteLLM knows under the very same vendor name, so the registry read like a
description of every provider and invited the next contributor to copy the
pattern for one more.

Mark those entries `standard` instead: the prefix is the name, the endpoint
comes from LiteLLM, and the entry keeps only what LiteLLM cannot know --
keywords for bare model names, the wizard's default model, the label. Zai
and MiniMax keep their env_key because LiteLLM names no variable for them,
which is exactly the kind of gap the registry exists to fill.

Model resolution now reads one property, ProviderSpec.model_prefix, instead
of three call sites each deriving it. A test asserts every `standard` name
is in litellm.provider_list, so the flag cannot outrun what LiteLLM knows.

Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
Routing knn through LiteLLM was meant to restore real streaming -- the old
direct client had none, so routed models emitted the whole reply as one
delta. Nothing asserted it, so the gain could regress unnoticed. Pin that
chunks arrive one at a time.

is_direct had no readers anywhere: make_provider picks AzureOpenAIProvider
by name. Drop the field and say in the Azure entry why that provider skips
LiteLLM.

Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
…ring

Four defects the review panel reproduced, all in the passthrough work:

Deriving environment variables from LiteLLM's "missing keys" leaked the
key. That list is every variable a vendor still wants, not the name of its
key: bedrock got the LLM key written into AWS_ACCESS_KEY_ID and
AWS_SECRET_ACCESS_KEY, cloudflare got it into CLOUDFLARE_API_BASE -- a
secret in an endpoint variable, process-wide, where any in-process boto3
call would pick it up as AWS credentials. Drop the derivation entirely: the
key already travels as an explicit api_key argument on every call, so the
environment never needed it. The five specs that had their env_key removed
get it back, since the TUI picker shows it as the variable to set.

Forcing `provider` to a passthrough vendor crashed on startup. That branch
still read the field directly, which hands back a raw dict for an extra,
so get_api_key() raised AttributeError. It now goes through the same
accessor as the rest of the function.

Model ids saved before the zai rename ("zhipu/glm-4.6") stopped resolving:
the alias covered the credential key but not the prefix. Specs can now
declare former names, and zai claims "zhipu".

A configured per-model override replaced the registry's entry instead of
layering over it, so setting top_p for Kimi silently dropped the
temperature that model requires. Registry first, config on top.

Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
…ides

The zai rename only reached the config file. `raven provider set zhipu`
answered "Unknown provider", a config that named zhipu as its forced
provider resolved to a name no spec matches, and a half-migrated file
holding both keys could still serve the stale one. Specs already declare
their former names, so resolve through them in one place and call it from
the write path, the accessor and the forced-provider branch alike.

Per-model overrides reached only the default provider. Routed knn models
build their own sub-providers and the sentinel planner builds its own, so
a configured override quietly did nothing there; both now inherit it.

Patterns match on substrings, which let a broad "kimi" shadow a precise
"kimi-k2.5" depending on which was written first. Longest match wins.

Rename agents.defaults.gateway to preferredGateway: `gateway` already
names the daemon's own config section, and the two have nothing to do with
each other.

Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
provider_section() existed only so two tests could pass a namespace where a
ProvidersConfig belongs -- production code paid for a test shortcut. The
stubs now build a real ProvidersConfig and the callers go straight to its
get(), which is the accessor that handles extras anyway.

configured_names() had no caller outside its own test. _setup_env kept a
branch for a spec with env_extras but no env_key, a combination no entry
has. Both gone.

The gateway-shortlist guard was pinned to openrouter, the gateway that
happened to have the bug; it now covers every gateway that ships a
shortlist, so the next one cannot repeat it.

Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
…ites

Round-three review found the previous fixes incomplete in four places.

The rename reached the two lookup helpers but not the six functions that
actually read and write config.json, so `provider set zhipu` reported
success while writing a second section the runtime never reads -- turning a
loud KeyError into a silently discarded key, and manufacturing the very
half-migrated file the same change defends against. Canonicalize at every
public entry point instead, read through the former name when only it is
present, and retire it on write so the file converges on one section.

The passthrough branch of provider matching kept returning the prefix
verbatim, so "zhipu/glm-4.6" resolved to a name no spec matches -- the
exact failure the forced branch had just been fixed for.

Keyword matching ignored an explicit vendor prefix, letting one vendor
claim another's model whenever the id contained its name:
"deepinfra/deepseek-ai/DeepSeek-V3" matched DeepSeek, wrote DeepInfra's key
into DEEPSEEK_API_KEY process-wide and rewrote the model id. A prefix names
the vendor outright, so it now decides alone.

The model picker derived the current provider from the raw config value,
which a config written before the rename spells the old way, leaving no row
highlighted.

Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
A full-diff review found the narrowing done earlier in this branch had
consequences in places the incremental reviews never looked at.

Prompt caching went silent for every model in the OpenRouter shortlist.
Those ids gained an "openrouter/" prefix in this same branch, and
token_wise asks find_by_model whether a model supports cache_control -- a
gateway prefix makes that return nothing. Both cache strategies now fall
back to keyword matching: caching belongs to the upstream vendor and
survives being reached through a gateway.

Registry parameter defaults went silent the same way for gateway-routed
ids, so Kimi lost its mandated temperature when reached via OpenRouter.

A bare `/model <id>` in the TUI derived no provider for an id whose vendor
has no spec, leaving whatever provider was forced before -- that provider's
key sent to a different vendor. It now hands routing back to auto.

Moonshot and MiniMax need their default_api_base after all: `provider test`
and the wizard probe /v1/models directly, before any LiteLLM call resolves
an endpoint. A test had pinned their absence, so it now only forbids
restating the model prefix.

`provider list` reported a provider stored under its pre-rename key as
unconfigured while `provider get` printed its key.

Drop preferredGateway. Routing a bare "gpt-4o" through OpenRouter needs its
id translated to "openai/gpt-4o"; sending "openrouter/gpt-4o" is simply
wrong, and the translation is not something this branch carries.

Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
…path

Round-five review, plus a bug an unrelated investigation turned up.

Provider lookup by name did not accept a former name, so `raven onboard
--provider zhipu` walked off a None spec and traced back mid-run, having
already written the key but not the model. Canonicalizing inside
find_by_name reaches every caller at once; patching call sites one at a
time is what left this one behind.

A gateway-prefixed model id resolved to no spec at all, which left the
model picker with no current provider highlighted, and let credential
resolution fall through to whatever vendor the id happened to mention --
that vendor's key, on a request LiteLLM sends to the gateway. A gateway
prefix now names the gateway as the provider, and matching stops there.

Configuring a vendor Raven has no spec for was read-only: `provider set`
raised, so the only way in was hand-editing config.json. The write path now
accepts any vendor LiteLLM names, and rejects the rest so a typo says so
instead of writing a section nothing reads. Every spec attribute it touches
tolerates a vendor without a spec -- `provider test` crashed on the last
one of those.

Also: the gateway-shortlist test could pass while checking nothing, and two
comments described behavior the code no longer had.

Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
Round-six review found the credential misrouting only half closed and the
caching fallback missing its third copy.

A model id prefixed with a vendor Raven has no spec for still fell through
to keyword matching, so "deepinfra/deepseek-ai/DeepSeek-V3" was served with
DeepSeek's key on a request LiteLLM sends to DeepInfra. Keyword matching may
now only confirm the vendor the prefix names, and the credentialed fallback
is limited to gateways and local deployments, which route whatever they are
handed. A bare model name still matches by keyword as before, and a gateway
still serves an upstream vendor's id.

Cache-control support was resolved in three places; the two in token_wise
gained a keyword fallback for gateway-routed ids, the provider's own copy
did not, so reaching Claude through Bedrock silently lost prompt caching.

The wizard needs a registry entry to guide anyone -- a default model, a
label, whether the provider takes a key or a login. Opening the write path
to any vendor LiteLLM names let those reach the wizard too, where the flow
walked off the missing spec. It now declines and names the command that does
configure them, and it resolves a former provider name so the wizard reads
and writes the same section: reading the pre-rename section by the typed
name found nothing, and a failed re-configuration then rolled back over a
real key with an empty one.

Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
A provider used to carry two names: ours and LiteLLM's, reconciled by a
litellm_prefix field at every call site that compared a name or a model-id
prefix. That field conflated two opposite facts -- LiteLLM spelling the same
vendor differently (vllm -> hosted_vllm) versus a provider reached through
another vendor's driver (SiliconFlow over OpenAI's) -- and "is this prefix
mine?" has opposite answers for the two. Deriving the distinction from whether
the prefix happened to be another spec's name was correct only by coincidence.

Adopt LiteLLM's spelling as the provider's own name instead, so there is one
name per provider and nothing to reconcile. hosted_vllm and ollama_chat are
renamed accordingly, with the previous spellings kept in name_aliases so saved
configs keep loading. litellm_prefix is gone; only a borrowed driver states
via_driver, and it is absent from route_names by construction, so a model id
asking for openai/ can no longer be answered with SiliconFlow's key.

Four facts were each implemented at several call sites and had already drifted
apart. They now live in registry.py alone: normalize_provider_name (name
spellings), split_model_id (prefix parsing), ProviderSpec.route_names (which
names refer to a provider) and ProviderSpec.claims (prefix beats keyword).
_match_provider's separate prefix and keyword loops collapse into one call to
claims, and its hand-rolled passthrough probing goes away now that
ProvidersConfig.get resolves a section under any spelling.

Also closes what the review panels found on top of that convergence:

- an empty declared section no longer answers for a provider, so credentials
  written under one of its other names are not shadowed by a placeholder;
- differently-spelled sections (azure-openai, OpenRouter) fold into their
  declared field at validation time rather than landing in extras where the
  always-present empty field wins;
- the management surface resolves a section the same way the runtime does, so
  a vendor LiteLLM hyphenates is no longer rejected by provider get/set;
- _litellm_knows returns three states instead of raising, because only one of
  its five call sites caught the exception;
- the onboard wizard no longer drops api_key and api_base for vendors Raven
  has no spec for;
- the benchmark harness requires an endpoint whenever the wire prefix names a
  borrowed driver, instead of trusting that a registered provider brings one --
  custom is registered and had none, which pointed the run at api.openai.com;
- raven status reads credentials from the loaded config, so ones supplied by
  environment variable are visible, and an OAuth provider without a token
  reports "not set" rather than a checkmark.

Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
Provider resolution had drifted apart across call sites because the tests
asserted the case that was last fixed rather than the invariant. Each sweep
here runs over every registered provider, so a rule that holds for one vendor
and not the rest fails at the vendor it breaks on.

Two of these sweeps were vacuous on the first pass: iterating route_names runs
the body zero times when the set is empty, which pytest scores as a pass, so
gutting route_names entirely left them green. They now assert the set is
non-empty first, and one anchors on model_prefix -- the string LiteLLM is
actually handed -- rather than on the set being iterated.

The agreement test between the config matcher and the registry was likewise
worthless in its first form: the matcher has enough downstream recovery
(spelling-insensitive section lookup, passthrough, local fallback) to reach the
right provider even with a wrong prefix rule, so an inlined comparison scored
green on all 21 providers. It now watches the call to ProviderSpec.claims,
which is the property worth asserting -- that the rule is consulted, not
restated.

The source guards are line scans, and they are tripwires rather than proofs: a
deliberate rewrite (partition instead of split, a spliced attribute name, an
aliased import) walks past them. What they catch is the shape that actually
recurred -- the same spelling copied to a new call site -- and they name the
single-source function in the failure message.

Every assertion here was checked by breaking the implementation and confirming
it goes red, including a replay of the defect where a gateway's key was sent to
the vendor named in the model id.

Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
…port

Accepting vendors with no registry entry made `list_providers` ask LiteLLM
whether a config section names a real provider or a typo, and that question
imports LiteLLM: about two seconds, plus the OpenAI SDK, httpx and tiktoken.
`raven status` went from 0.5s to 3.3s for anyone who had configured such a
vendor -- so the more a user relied on the capability this branch adds, the
slower their CLI became. The eager-import cost had been removed from the CLI
before; this brought it back on the reporting paths.

Snapshot LiteLLM's provider names into the package instead. A name it contains
is answered for free, which covers configuring and reporting alike, and the
reporting path stops there: a stale snapshot costs one row in a table, whereas
importing LiteLLM to render that table costs every caller two seconds. Anywhere
the answer decides whether to write or reject, a miss still falls through to
LiteLLM itself, so a vendor added in a newer release than the snapshot remains
configurable.

A guard test asserts the snapshot equals the installed LiteLLM's list, in both
directions: a name only in the snapshot would let a typo through as a vendor,
and a name only in LiteLLM would hide a working provider from `provider list`
and from the startup gate, sending a configured user back into the wizard. A
second test asserts the reporting path does not import LiteLLM at all, since
that is the property the snapshot exists to provide.

Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
`model_overrides` was added to `LiteLLMProvider` and wired into two of the three
places that build one. The evolver builds its own, so a model that needs a
particular temperature got it from the agent and not from taxonomy induction --
the same parameter silently meaning two different things depending on which code
path ran.

A guard test now requires every production construction site to pass the
config's overrides. The per-endpoint builder is exempt and says why: it inherits
the fallback provider's rather than reading config itself.

Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
AiHubMix is reached through the OpenAI driver and wants the vendor's bare model
id, so the routing segment is stripped before re-prefixing. It was stripped by
keeping the last path segment, which is only the same thing for a two-segment
id: a model id that carries a slash of its own ("openai/gpt-oss-120b" is Groq's
name for it) was truncated, and the gateway was then asked for a model that does
not exist under that name.

Drop exactly the one leading segment instead. Two-segment ids are unaffected,
which is every id in the shipped shortlists; the change is visible only where an
id had three or more segments.

Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
Whether a config key names a given provider was answered two ways. The runtime
compared forwards, camelCasing the field name; the management surface compared a
normalized form, and normalizing does not decompose camelCase -- "azureOpenai"
folds to "azureopenai", never to "azure_openai". So a section stored in camelCase
was read by the runtime and invisible to `provider get` and `provider set`.

Worse than invisible: because the write path recognised no such key, it did not
retire it, and added a second section under the field name. The declared field is
always present, so the newer and empty one then won the merge and the credential
in the file became unreachable. On the base branch the write was simply ignored
and the key survived, which makes this a regression introduced by the section
folding earlier in this branch, not a pre-existing hole.

Both paths now call `names_same_provider`, which answers it once: exact match,
the camelCase form of the snake name compared case-insensitively, or the
normalized spelling. Still built forwards from the field name -- splitting a key
on its capitals cannot tell "azureOpenai" (two words) from "OpenRouter" (one).

Merging two spellings also stops taking the winner verbatim. The current name
still wins a genuine conflict, but an unset value no longer counts as one, so a
placeholder cannot erase what the user wrote under the provider's other
spelling. This covers configs an older build already left with two sections,
which no amount of care on the write path can undo.

Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
@0xKT
0xKT force-pushed the refactor/provider_litellm_unification branch from 11468f0 to 13368e0 Compare July 30, 2026 14:11
@0xKT
0xKT merged commit 16bdca7 into main Jul 30, 2026
9 checks passed
@0xKT
0xKT deleted the refactor/provider_litellm_unification branch July 30, 2026 14:29
0xKT added a commit that referenced this pull request Jul 31, 2026
## Summary

Bump the package version from 0.1.9 to 0.1.10 (patch release). 22 PRs
merged since v0.1.9, no breaking changes.

Features:

- #260 feat(tui): list the providers that work, and the rest one level
down
- #252 feat(*): offer every supported provider in the onboarding picker
- #251 feat(tools): read images with read_file, and fix four silent
type-check bugs
- #239 feat: add shell command approval flow
- #217 feat: rework the TUI transcript into collapsible episodes
- #220 feat(cli): nudge raven upgrade in the tui status bar when behind
- #209 feat(providers): add MiniMax Global and CN OAuth

Refactors:

- #259 refactor(*): one answer to which credentials a provider needs
- #249 refactor(providers): unify provider management on litellm

The remaining PRs are fixes (#255, #256, #258, #253, #238, #226), test
work (#236, #230, #224), docs (#250, #215, #200), and benchmark tooling
(#207).

## Type

- [ ] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [x] Other

## Verification

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [ ] User-facing docs or screenshots are updated when needed

Bump is limited to `pyproject.toml` and `uv.lock` (`uv lock` sync).
Local preflight covers the branch CI checks (commit lint, PR title and
body lint, ruff, large-file gate).

## Risk

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

Version-only change; no code or behavior change. Rollback is a revert of
this commit.

## Related Issues

N/A

Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants