Repository navigation
fix(llm): --provider openai sends a model litellm does not know as OpenAI's to the OpenAI endpoint - #256
Conversation
…ai/<model>
resolve_model sent every --provider openai model to litellm bare, and
litellm infers OpenAI only for names on its own lists. glm-5.3 behind
OPENAI_API_BASE, an org/name id (meta-llama/Llama-3.1-8B) or a model
newer than the installed litellm was refused before any request ("LLM
Provider NOT provided"); a bare claude-* name went to Anthropic. So
`eval cross-judge --judges openai/glm-5.3` scored nothing and read
NOT MEASURED (intel#136), and answers, the judge, the panel and the question
and persona generators failed the same way.
Such names now go out as openai/<model>. A name litellm already sends to
OpenAI (its chat and text-completion lists) or one with a litellm
provider prefix (openai/..., azure/...) keeps its route, so its
response-cache key, which holds the litellm model string, is unchanged.
llm.py is at its growth baseline: the module docstring's closing
paragraph, which repeats the opening one, and a comment that repeats the
legacy-section header are dropped, so the file stays at 1118 lines.
- tests/test_openai_compatible_route.py: the production adapter with
litellm's real provider lookup (mock_response, nothing sent), for
LLM(...).ask and eval cross-judge end to end; fails on main.
- tests/test_llm.py: route rows for the prefixed names and for the ones
that must not change.
Fixes intel#136
Signed-off-by: Vladislav Perevezentsev <vladislav.perevezentsev@intel.com>
…penai Nothing in the docs said how to point dokimi at vLLM or a gateway; the intel#136 reporter found the openai/openai/<model> form by trial. The setup paragraph now names OPENAI_API_BASE / OPENAI_BASE_URL, says any served model name works (--judges openai/glm-5.3), that OPENAI_API_KEY must be set even when the server ignores it (the request fails with "Missing credentials" otherwise), and that cost is unknown for such a model, so a --max-usd run stops after its first call. Signed-off-by: Vladislav Perevezentsev <vladislav.perevezentsev@intel.com>
…t as gpt-*/o* A gpt-* name off litellm's OpenAI lists (gpt-7-new) gets openai/ too, so the fragment's "(gpt-*, o*) keeps its route" overstated it. Signed-off-by: Vladislav Perevezentsev <vladislav.perevezentsev@intel.com>
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #256 +/- ##
==========================================
- Coverage 93.26% 93.26% -0.01%
==========================================
Files 272 272
Lines 35117 35121 +4
Branches 5964 5964
==========================================
+ Hits 32752 32754 +2
- Misses 1547 1548 +1
- Partials 818 819 +1
🚀 New features to boost your workflow:
|
napetrov
left a comment
There was a problem hiding this comment.
The fix for glm-5.3 and meta-llama/… works, and the tests pass (100 in test_llm and test_openai_compatible_route; ruff, growth ratchet and import-architecture clean).
One thing to settle before approval: the openai branch in dokimi/llm.py:146-149 also reroutes bare names that litellm sends to other providers today. That is 449 catalog names: bedrock 351 (e.g. amazon.nova-pro-v1:0), vertex_ai 46, anthropic 22, cohere 16, ai21 12, nlp_cloud 2. They work on main with the cloud's credentials, and after this PR they would go to OPENAI_API_BASE. The body's "numbers do not change for a model that worked before" does not cover them. I checked this against litellm 1.106.0 from the lock; I did not run it against live Bedrock. Options: narrow the rule to names litellm cannot route, or list the affected set in the body and changelog and add a test row that pins one bare Bedrock id.
Nits:
README.md:152-153says any served model name works. An org prefix that matches a litellm provider (deepseek/,groq/,ollama/,together_ai/) still routes natively and ignoresOPENAI_API_BASE. Say "a name without a litellm provider prefix", or tell users to writeopenai/<name>.README.md:155describes--max-usdfor every--provider openairun. Onlynoveltytakes it (SpendCap,novelty/client.py:46-80).
antonwolfy
left a comment
There was a problem hiding this comment.
The fix does what it says for glm-5.3 and meta-llama/…, and CI is green. One point from @napetrov is still open and I confirm it, so this does not approve yet. One nit inline.
I agree with @napetrov that the openai branch also reroutes bare names that litellm sends to other providers. New evidence: at this head resolve_model returns openai/claude-sonnet-4-5, openai/amazon.nova-pro-v1:0 and openai/gemini-2.5-flash for provider openai, where litellm.get_llm_provider on the bare name gives anthropic, bedrock and vertex_ai, and ft:gpt-4o-mini:org::id becomes openai/ft:gpt-4o-mini:org::id.
I also agree with @napetrov's README points: the "any model name works" sentence is false for deepseek/, groq/ and ollama/ prefixes, and --max-usd exists only on novelty.
What I ran, on a Linux review host at 81c0d01:
make ci-local: 28 passed, 0 failed, 7 skipped (jobs that need a PR event or tools not installed locally).- A reproduction of the reroute above with the LLM transport mocked.
- Mutations of the new branch: dropping the text-completion list, always prefixing, and letting any
/pass are all caught; replacing theprovider_listcheck within ("openai", "azure")passes the fulltest_llm.pyandtest_openai_compatible_route.py.
Checked, no issues
- Callers:
resolve_modelfeeds pricing, usage andLLM._route;openrouter,openai-codex,google-vertexandamazon-bedrockreturn before the new branch and are unchanged. - Kept routes:
gpt-4o,o3,gpt-5,gpt-3.5-turbo-instruct,azure/gpt-4oandopenai/glm-5.3come out unchanged, so theopenai/openai/<model>workaround still works. - Repo rules: the changelog fragment passes its check,
llm.pyis at its 1118-line growth baseline, new files carry the SPDX header, and the new tests run offline. - Env vars: litellm reads both
OPENAI_BASE_URLandOPENAI_API_BASE, as the README says.
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
…der openai resolve_model kept a bare name bare only when it was on litellm's OpenAI chat or text-completion lists. litellm also takes a fine-tune id (ft:gpt-4o-mini:org::id; its test is "ft:gpt-3.5-turbo" or "ft:gpt-4" in the name) for OpenAI's, so such a name went out as openai/ft:... : the same endpoint, but a new response-cache key, which the changelog said would not happen. The bare test now repeats litellm's, so the name keeps its route and its cache entries. litellm.get_llm_provider is not called instead: on a name it cannot place (glm-5.3, the case this fixes) it prints a "Provider List" banner to stdout. - tests/test_openai_compatible_route.py: names kept bare (gpt-4o, gpt-3.5-turbo-instruct, ft:gpt-4o-mini:org::id) still route through litellm's real lookup, and a fine-tune litellm does not place (ft:gpt-5-mini:org::id) gets openai/, so a looser ft: test fails. Signed-off-by: Vladislav Perevezentsev <vladislav.perevezentsev@intel.com>
…t on provider openai Review of intel#256: the openai rule also reroutes the bare names litellm assigns to another cloud (Bedrock, Vertex, Anthropic, Cohere, AI21). That is intended: on main such a name reached that cloud with OPENAI_API_KEY passed as its key, which Anthropic and Bedrock refuse. The row pins amazon.nova-pro-v1:0 -> openai/amazon.nova-pro-v1:0 next to the claude-* one. Signed-off-by: Vladislav Perevezentsev <vladislav.perevezentsev@intel.com>
…that only novelty takes --max-usd Review of intel#256. "Any model name the server serves works" was false for a name with a litellm provider prefix (deepseek/, groq/, ollama/): it keeps that provider's route and ignores OPENAI_API_BASE, so the text says to write it as openai/<name>. It also says that a bare claude-*, Bedrock or Vertex id now goes to the server, and needs its cloud's provider to reach that cloud. --max-usd is a novelty flag (SpendCap), not one every --provider openai run takes. Signed-off-by: Vladislav Perevezentsev <vladislav.perevezentsev@intel.com>
…he openai endpoint Review of intel#256. The fragment said a bare claude-* name "went to Anthropic instead" and left out the rest of that set: about 450 bare names in litellm 1.106 (Bedrock, Vertex, Anthropic, Cohere, AI21) that went to their cloud with OPENAI_API_KEY as the key and worked only with it unset. A Fixed entry now names the set, why it moves and which provider reaches those clouds. The first entry no longer says "any model name", lists a ft: fine-tune among the names that keep their route, and the README entry matches the reworded paragraph. Signed-off-by: Vladislav Perevezentsev <vladislav.perevezentsev@intel.com>
Signed-off-by: Vladislav Perevezentsev <vladislav.perevezentsev@intel.com>
…nai/gpt-x test_grader_characterization (intel#251) pins the response-cache file of a judge and a panel rerun at model gpt-x, provider openai. gpt-x is off litellm's OpenAI lists, so this branch sends it as openai/gpt-x, and the key, which holds the litellm model string, changes with it. The new names are the keys main computes for model openai/gpt-x: only the route moved, the messages and draws behind the key are the same. Signed-off-by: Vladislav Perevezentsev <vladislav.perevezentsev@intel.com>
|
@napetrov thanks. Taken as your second option: the rule stays, and the set is named and pinned. I kept the rule rather than narrowing it because those names do not work on main once
So they reach their cloud only while README nits, both fixed in |
|
@antonwolfy thanks for the mutation run and the Your Also: |
litellm also routes providers from its openai_like JSON registry (crusoe/, sarvam/, ...), which are not in provider_list. On provider openai such a name got openai/ and went to OPENAI_API_BASE instead. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
|
README.md conflicted because intel#271 moved the credentials paragraph this branch extends into docs/reviewing.md; README.md is main's, and this branch's OpenAI-compatible server text is carried verbatim into that paragraph in docs/reviewing.md. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
|
Merged main after #271 (f8279bf): #271 moved the README credentials paragraph this PR extends into |
antonwolfy
left a comment
There was a problem hiding this comment.
LGTM. The route rule holds up at this head: the ft:gpt-* regression, the ollama/ row and the Bedrock row from the earlier round are in, and the JSON-registry prefix fix (crusoe/) is right. Two nits inline.
@xaleryb's question on the response-cache key waits for the author: the route now depends on litellm's cost map, so a model that is in the remote map but not in the installed backup gets a different key depending on whether the fetch succeeds.
What I ran, on a Linux review host at f8279bf:
make ci-local: 27 passed, 1 failed, 7 skipped; 10,196 tests; the changed-line coverage gate passes. The one FAIL ispip-audit, which listsfilelock 3.32.7advisories from the lock files; this PR does not touch them.resolve_modelon edge names against litellm's ownget_llm_provider(bare and prefixed names, JSON-registry andprovider_listprefixes,ft:variants), with the LLM transport mocked.- A mutation run over the new branch against
test_llm.py,test_openai_compatible_route.pyandtest_grader_characterization.py.
Checked, no issues
- Callers:
resolve_modelfeeds pricing, usage andLLM._route;openai/<x>is a valid litellm route for cost and completion, andopenrouterandopenai-codexstill return before the new branch. - Repo rules: all ten non-merge commits carry
Signed-off-by;llm.pystays at its 1118-line baseline; the new test file has the SPDX header and the tests write nothing outsidetmp_path; the changelog fragment check passes. - Docs:
--judges,novelty --max-usd(SpendCapstops after the first call when cost is unknown) and both base-URL variables match the code.
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
antonwolfy
left a comment
There was a problem hiding this comment.
LGTM. I pushed a commit per nit, up to 5b6b90c, which fix the two nits from my review:
01406a0:dokimi/tests/test_llm.py:62: added theft:gpt-3.5-turbo:org::idrow after theft:gpt-4o-minirow.5b6b90c:dokimi/changelog.d/20261027_120000_vlad-perevezentsev_openai-compatible-model-route.md:23: the entry's lead-in now namesdocs/reviewing.mdinstead of README.
make ci-local at 5b6b90c: 27 passed, 1 failed, 7 skipped.
Not merging yet: @xaleryb on the cache-key scheme: the openai route depends on litellm's cost map (remote vs installed backup), so one model and prompt can have two response-cache keys; hash the caller's bare name, or always use openai/ waits for your answer, @vlad-perevezentsev.
… on its route Review of intel#256 (xaleryb): the openai route of a bare name depends on litellm's price map. litellm loads the remote map, or the installed backup under LITELLM_LOCAL_MODEL_COST_MAP=True or offline, and the two disagree on a new model: gpt-6.1-terra is on the remote one's OpenAI list (206 names) and not on the backup's (197), so it went out bare in one run and as openai/gpt-6.1-terra in the other. Both reach the same endpoint, but the response-cache key held the route, so one model and prompt had two keys, and a rerun on the other map paid for every call again and could give other numbers. On provider openai the key now holds self.model, which is what main keyed on, since main sent every openai name bare. Every key main wrote for that provider still hits, including those of the names this PR reroutes, and the map no longer moves it. Other providers are unchanged. OPENAI_API_BASE was never part of the key, so keying on the name loses nothing the route held. - test_grader_characterization.py: the two file names ea2acaa re-pinned for openai/gpt-x are back to main's, so the file matches main again. - test_llm.py: main's key for gpt-6.1-terra (routed openai/ under the installed map the tests use), recorded by running main's code. - test_openai_compatible_route.py: a reply stored with the model on the map replays once it is off the map and the route is openai/. - llm.py stays at 1118 lines: the response-cache paragraph of the module docstring, which names the key's parts, is rewrapped to the project's 100 columns. Signed-off-by: Vladislav Perevezentsev <vladislav.perevezentsev@intel.com>
|
@xaleryb thanks for Your second option would change every stored OpenAI key once and the route of every OpenAI model, so I left it out. Tests:
Keying on the route again fails all three. |
|
🔍 OpenCodeReview found 2 issue(s) in this PR.
|
| import litellm | ||
| known = litellm.open_ai_chat_completion_models | litellm.open_ai_text_completion_models | ||
| ours = model in known or "ft:gpt-3.5-turbo" in model or "ft:gpt-4" in model # as litellm | ||
| routed = (p := model.split("/", 1)[0]) in litellm.provider_list or litellm.JSONProviderRegistry.exists(p) if "/" in model else ours |
There was a problem hiding this comment.
Nit: this one-liner mixes a walrus, or and a trailing conditional expression, and ours is computed even when the name has a /. It parses as intended: (p in provider_list or JSONProviderRegistry.exists(p)) if "/" in model else ours. Separate branches would be easier to review. It also reads litellm.JSONProviderRegistry unguarded, and pyproject.toml only pins litellm>=1.105.0. If that attribute is missing in some supported version, every provider=openai name containing / (e.g. meta-llama/Llama-3.1-8B) raises AttributeError instead of being routed. Confirm the attribute exists at the minimum pin, or raise the pin.
Suggestion:
| routed = (p := model.split("/", 1)[0]) in litellm.provider_list or litellm.JSONProviderRegistry.exists(p) if "/" in model else ours | |
| if "/" in model: | |
| prefix = model.split("/", 1)[0] | |
| routed = prefix in litellm.provider_list or litellm.JSONProviderRegistry.exists(prefix) | |
| else: | |
| routed = model in known or "ft:gpt-3.5-turbo" in model or "ft:gpt-4" in model # as litellm |
| ("22510fd74bf01210757177f302f9b6728b81e9e3b31572bd6245f0c88af78dfd", | ||
| lambda: get_llm("openrouter", "openai/gpt-oss-120b", "k", temperature=0.0).invoke( | ||
| "ask", draw=1)), | ||
| # Off the installed price map, so routed as openai/gpt-6.1-terra here; main sent it bare. |
There was a problem hiding this comment.
Nit: this entry is named openai-rerouted, but the key is built from the caller's name on openai, so the hash is the same whether resolve_model returns gpt-6.1-terra or openai/gpt-6.1-terra. If resolve_model stopped rerouting, the test would still pass. It only pins key stability, which is fine. The comment is also fragile: it stops being true once litellm lists gpt-6.1-terra. Rename the entry and reword the comment, or leave routing to the route tests.
Summary
Fixes #136. With
--provider openai, a model name that litellm does not take for OpenAI's now goes out asopenai/<model>, which means toOPENAI_API_BASE. A name litellm already sends to OpenAI keeps its route. On provider openai the response-cache key holds the model name as given, as on main, so every stored entry still hits.The
--judgesspec is split correctly. The provider is lost inllm.resolve_model, which sent everyopenaimodel to litellm bare. litellm infers OpenAI only for names on its own lists and forft:gpt-*fine-tunes. It refused any other name before sending a request ("LLM Provider NOT provided"):glm-5.3behindOPENAI_API_BASE,meta-llama/x, a model newer than the installed litellm. A bare name it assigns to another cloud (claude-*,amazon.nova-pro-v1:0,gemini-*) went to that cloud with the OpenAI key. Every--provider openai/--judge-provider openaipath goes throughresolve_model: answers, judge, panel, novelty, question and persona generation.The
openai/openai/<model>workaround keeps working.Also:
6296697): the model name as given, not the route. The route of a new model depends on litellm's price map. The remote map and the installed backup (LITELLM_LOCAL_MODEL_COST_MAP=True, or offline) disagree:gpt-6.1-terrais on the remote OpenAI list (206 names) and not on the backup (197), so it goes out bare under one and asopenai/gpt-6.1-terraunder the other. Both reach the same endpoint, and the key stays the same under either map. It is also the key main wrote, because main sent every openai name bare.docs/reviewing.md(the README paragraph moved there in docs: illustrated intro on docs home, short README with start-here #271): how to use an OpenAI-compatible server. It coversOPENAI_API_BASE/OPENAI_BASE_URLand which names reach the server. A name with a litellm provider prefix (deepseek/,groq/,ollama/) keeps that route, so it should be writtenopenai/<name>.OPENAI_API_KEYmust be set even if the server ignores it. Cost is unknown for such a model, sonovelty --max-usdstops after one call.llm.pyis at its growth baseline: two passages that repeat others are dropped (the docstring's closing paragraph and a comment), and the file stays at 1118 lines.Evidence
Live
eval cross-judge(3 questions, 2 arms) against a loopback stub/v1/chat/completionsviaOPENAI_API_BASE. On mainb226dba,--judges openai/glm-5.3givesskill: NOT MEASUREDand exit 0, and litellm prints its "Provider List" banner 12 times. "After" is head6296697.--judgesopenai/glm-5.3openai/meta-llama/Llama-3.1-8Bopenai/amazon.nova-pro-v1:0openai/openai/glm-5.3(workaround)openai/gpt-6.1-sol(control)openai/ft:gpt-4o-mini:org::id(control)With
--provider openaion main and the transport intercepted (litellm 1.106.0), a bare name of another cloud reached it with the OpenAI key:OPENAI_API_KEYsetclaude-sonnet-4-5x-api-key: <OpenAI key>ANTHROPIC_API_KEYamazon.nova-pro-v1:0Authorization: Bearer <OpenAI key>Tests:
tests/test_openai_compatible_route.py(new) runs the production adapter with litellm's real provider lookup (mock_response, nothing sent). It coversLLM(...).askfor names that getopenai/and for names kept bare,eval cross-judge --judges openai/glm-5.3end to end, and a reply stored with the model on the price map that replays once it is off the map.tests/test_llm.py: route rows for the prefixed names (includingclaude-*and a bare Bedrock id) and for the names that must not change (gpt-6.1-sol,gpt-3.5-turbo-instruct, bothft:families,openai/glm-5.3,azure/gpt-4o,ollama/llama3,crusoe/x).test_an_entry_main_wrote_is_hitgets main's key forgpt-6.1-terra, which is routedopenai/under the installed map.tests/test_grader_characterization.py(refactor(eval): grade the judge and panel loops with llm.ask_parsed #251) is as on main: its pinned cache-key file names hold forgpt-x, now routedopenai/gpt-x.10 failed, 101 passed(BadRequestError: LLM Provider NOT provided, and the routes). On this branch:111 passed.ft:test, loosening it to anyft:, always prefixing, taking any/as a route, keeping bare names off the lists, keying the cache on the route. Replacing theprovider_listtest within ("openai", "azure")is caught by theollama/row.make ci-localat6296697: 27 passed, 1 failed, 7 skipped (pytest 10146 passed). The FAIL ispip-audit, which reportsfilelock 3.32.7advisories from the lock files; this PR does not touch them. The docker smoke passed in round 1 and was not rerun, since nothing docker-related changed.Merge Danger
Door: two-way. A revert restores the bare route; no records or schemas change.
Blast Radius: names that failed before every request, plus one set that changes route. A bare name litellm assigns to another cloud now goes to
OPENAI_API_BASE: about 450 in litellm 1.106 (Bedrock 351, Vertex 46, Anthropic 22, Cohere 16, AI21 12, NLP Cloud 2). On main it reached that cloud only whileOPENAI_API_KEYwas unset. With the key set, the OpenAI key went to that cloud and was refused. Those clouds have their own providers.Overlap: main is merged in (
f8279bf, after #271). #251's characterization test is unchanged from main. #255 edits other hunks ofllm.py, and a one-line merge may be needed next to it.Not in this PR:
utils.get_llm'sopenaibranch has the same bare route, but no caller in the package reaches it, and Consolidate duplicated subsystems (stats, LLM routing, judge loop, containers, digests, writers) #229 P1-2 removes it.eval/cross_judge.py:343swallows judge errors with no log, so a judge failing every call givesNOT MEASUREDwith no reason.Checklist
pytestpassesruff,mypy, and schema checks passAGENTS.md.github/.licenserc.yaml)