Repository navigation
feat(decisions): add Laya and Bespoke providers to the Add Model form - #45260
Conversation
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".
|
|
|
|
Merging this PR will not alter performance
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
…ision_providers Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> # Conflicts: # litellm/decisions/main.py
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ision_providers Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> # Conflicts: # litellm/__init__.py # litellm/_lazy_imports_registry.py # litellm/utils.py
|
#45335 is merged and already adds the TypeSafe and Strands Decider entries, so this PR needs a rebase that drops those two |
…oreply.github.com>
…'s TypeSafe Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
@mateo-berri I merged main and dropped our TypeSafe and Strands Decider copies, so main's versions win. What's left in this PR is Laya, Bespoke, the Evaluation mode and the TypeSafe cost case |
…ision_providers # Conflicts: # ui/litellm-dashboard/src/components/add_model/handle_add_model_submit.test.tsx
|
bugbot run |
…ision_providers # Conflicts: # litellm/__init__.py # litellm/_lazy_imports_registry.py # litellm/utils.py # tests/integration/cost_calculation/cost_tracking_case.py # tests/integration/cost_calculation/cost_tracking_cases.json # tests/integration/cost_calculation/test_cost_tracking.py # tests/unit/proxy/decisions_endpoints/test_endpoints.py
|
@litereview code-review |
✅ code-review5/5 · Ready for human reviewScope: full · Full PR review. No correctness findings. Checks and execution details
Manual E2E evidence. The PR description includes inspectable manual E2E proof run on
Gap between QA and head. Head Tests I ran locally (all passed): CI on head: GitHub check-runs for Not relied on: the new
No other callers of
View review · Reviewed |
|
bugbot run |
…rver Route laya and bespoke Decisions deployments through the same oss_connection and validate_oss_model rules the pass-through and the classifier router use, so LAYA_API_KEY and BESPOKE_API_KEY only go to LAYA_API_BASE and BESPOKE_API_BASE, an api_base with credentials, a query, or a non-HTTP scheme answers 400, and an unknown checkpoint name answers 400 before any upstream call
|
@litereview code-review |
|
bugbot run |
|
@litereview code-review |
…el on the env server A deployment added through Add Model always carries an API base, so the env key only reached deployments with no base. The env key now goes to a deployment whose base matches LAYA_API_BASE or BESPOKE_API_BASE, an empty deployment key counts as no key, and the Bespoke placeholder is the name Ollama serves.
|
@litereview code-review |
…lama users at bespoke/nimble
|
@litereview code-review |
…ough on a /v1 base
|
@litereview code-review |
|
bugbot run |
|
@litereview code-review |
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit d4d74f0. Configure here.
TLDR
Problem this solves:
laya/*andbespoke/*deployments aren't served by the Decisions routes at allHow it solves it:
layaandbespokeas Decisions providers calling<api_base>/v1/systemonelayaandbespokerows to the decisions wire integration testDecisions - /v1/decisionsmode is already on main, so this PR does not carry itLAYA_API_KEYorBESPOKE_API_KEYonly to a deployment or Auto Router classifier onLAYA_API_BASEorBESPOKE_API_BASE, and a key typed in the form always wins/v1calls the same/v1/systemonefrom a deployment, an Auto Router classifier, and the/layaand/bespokepass-through routes, where main's classifier and pass-through called/v1/v1/systemonebespoke/nimbleon OllamaDecisions this PR makes. The env key goes to a deployment or an Auto Router classifier only when its API base is the env base, ignoring a trailing
/or/v1since both call the same URL. Main sent it only when no API base was set, so a classifier whose explicitapi_baserepeats the env base now sends the key to that server where main sent none. Sending it to any base would hand the operator's key to whatever server a deployment names, the wayOPENAI_API_KEYgoes to an explicitapi_base, and never sending it to an explicit base would leave the env key unusable from the form, which always sets one. The env var docs change with this in BerriAI/litellm-docs#2356.laya/*andbespoke/*are priced at 0 in the cost map, the same as Strands Decider, because they are self-hosted and nobody bills per token. An admin who wants spend sets Custom Pricing on the deployment, which the QA below shows, while a non-zero default would bill a cost nobody charges. The checkpoint allowlist matches the cost-map rows, so an alias likelaya/autoor an Ollama tag likenimble:latestanswers 400 instead of reaching a server under a name with no priceUser Flow
Before: an admin who self-hosts Laya or Bespoke Nimble can't add it from the Add Model form, and the model can't be called even when added by hand
LayaorBespokeinto the Provider dropdown: "No providers found""model": "laya/english"and their API base: 500, "model id(s) ... are not live in this pod's router", the same forbespoke/nimble"model": "laya/english"answer 400 "Invalid model name passed in", the same forbespoke/nimbleAfter: the admin adds both from the form, calls them straight away, and sees the spend they priced
laya/english, set Mode toDecisions - /v1/decisions, enter the API base and API key, turn on Custom Pricing under Advanced Settings with an input and an output cost, and click Add Model: "Model laya-form-priced created successfully"x-litellm-response-cost, and the calls show their cost on http://localhost:4000/ui/logsbespoke/nimbleon Ollama, and the added model answers on both routes with its token usageLAYA_API_BASE, written with or without a trailing/or/v1, works with the API Key field left empty, because the proxy sendsLAYA_API_KEYthere and nowhere elsePre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
uv run pytest tests/unit/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or moreDelays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
Last updated: d4d74f0. QA ran on d4d74f0, the last commit with a product diff
Shared setup, identical on both sides apart from the commit the proxy and the dashboard were built from
--num_workers 2,STORE_MODEL_IN_DB=True, an emptymodel_list,LITELLM_LOCAL_MODEL_COST_MAP=True, its own empty Postgres database, and the dashboard dev server from the same tree rewriting to it, so the Add Model form is the real one. The after side also setsLAYA_API_BASEandLAYA_API_KEYto the keyed Laya serverhttp://127.0.0.1:48029(401 without it), Laya serve with no key onhttp://127.0.0.1:47179, Ollama onhttp://127.0.0.1:35867serving Bespoke Nimble, and Strands Decider onhttp://127.0.0.1:48559. TypeSafe, Perplexity, and OpenRouter are the hosted APIs{"model":"<model>","state":"ticket: export hangs at 99%","questions":{"is_defect":{"type":"noul","instructions":"Is this a defect?"}}}and OpenAI body{"model":"<model>","input":"ticket: export hangs at 99%","questions":[{"type":"predicate","name":"is_defect","instructions":"Is this a defect?"}]}Before (e226016, the merge base)
GET /public/providers/fieldslists 132 providers withtypesafeandstrands_decideramong them, and nolayaorbespokeLayaorBespokein the Provider dropdown shows "No providers found" (screenshots in User Flow)bespoke/nimbleonhttp://127.0.0.1:35867answers the same 500, andGET /v1/modelsreturns{"data":[],"object":"list"}bespoke/nimbleanswers the same two 400sAfter (d4d74f0)
A. Laya with the key typed in the form and Custom Pricing of 1 input and 2 output per 1M tokens. The toast reads "Model laya-form-priced created successfully" and the form sends
The cost is 38 input tokens at $1 per 1M. The server reports 0 output tokens, so the output price adds nothing
B. Laya on the env server with the API Key field empty and a trailing slash on the API base. The form sends
{"model":"laya/english","custom_llm_provider":"laya","api_base":"http://127.0.0.1:48029/"}with noapi_key, and both routes answer 200 from the server that answers 401 without its key/v1/decisionsanswers the same 200B2. The same env server written as
http://127.0.0.1:48029/v1, API Key field empty. The form sends{"model":"laya/english","custom_llm_provider":"laya","api_base":"http://127.0.0.1:48029/v1"}with noapi_key, and both routes answer 200 (screenshot in User Flow)/v1/decisionsanswers the same 200, and the keyed server's log shows onePOST /v1/systemoneper call, never/v1/v1/systemoneC. Laya on the other server with no key (
laya-open, API basehttp://127.0.0.1:47179) answers 200 on both routesD. Bespoke Nimble.
GET /public/providers/fieldslistsbespokeas "Bespoke Nimble" with placeholderbespoke/nimble, and the form's model list offersbespoke/nimble-latest,bespoke/nimble, andbespoke/bespokelabs/Bespoke-Nimble-9B. The form sends{"model":"bespoke/nimble","custom_llm_provider":"bespoke","api_base":"http://127.0.0.1:35867"}and both routes answerT. With Bespoke Nimble picked, resting the pointer on the API Base help icon shows "On Ollama, pick bespoke/nimble as the model" (screenshot in User Flow), the same text the form reads from the API
E. The decision providers already in the form, each added through it with Mode
Decisions - /v1/decisions. Strands Decider has Custom Pricing of 0.5 input and 1.5 output per 1M, and the other three use their bundled pricesStrands Decider's two calls, on the same boot as the rest
F. Bad deployment values save through
/model/newand answer 400 at call timehttp://localhost:/ui/logs shows the
laya-form-pricedcalls at $0.000038, TypeSafe and OpenRouter at $0.000012, Perplexity at $0.000002, and the unpriced Laya and Bespoke calls at "-" (screenshot in User Flow)Things the run noticed
/model/newsaves bad checkpoints, rejected at call time?tab=add-modelopens Deployed Models, not Add Modellaya/english, not the public name/v1, the call drops it/live-pr-risk (d4d74f0 against the merge base e226016)
Each side ran 2 workers on its own Postgres, with real Laya (keyed and keyless), Ollama Bespoke Nimble and Strands Decider upstreams behind forwarding recorders, and four boots per side for
LAYA_API_BASEset to a plain base, a/v1base,http://v1andhttp://v1/v1. Every cell made exactly one upstream attempt on both sidesBreaking. None. The first run found that a base whose host is named
v1(http://v1) lost its host in the/v1strip, and d4d74f0 fixed it by stripping/v1from the URL path only. Classifiers and the/layapass-through now reach av1host on every cell, and 2,073 generated bases (IPv6, ports, percent-encoding, upper case) come back as the checked base minus a trailing/v1pathBackward incompatible. An explicit classifier
api_basethat names theLAYA_API_BASEorBESPOKE_API_BASEserver now gets that server's env key. Base sent no key there and fell back to the heuristic scorer after a 401, and head classifies. Every Auto Router call answered 200 on both sides, and only the router cause header and one extra $0 classifier spend row changed. This follows theOPENAI_API_KEYconvention inlitellm/main.py, where an env key goes with an explicitapi_base, narrowed here to the env server, and shipped in e3452c5. The other server never got the env key in any cell (deployments, health checks, classifiers). A classifier or pass-through on a/v1base now calls/v1/systemoneonce, where base called/v1/v1/systemoneand got 404Regression risk. These paths are untested and traced only,
laya/englishmatching a fallback entry keyedenglish,_infer_valid_provider_from_env_varscountingLAYA_API_KEY, and the rate limiter labels. A laya health check writes a $0acompletionspend row, the same kind Strands' health check writes on baseDependency graph. These were verified live on both sides, the classifier client (Auto Router chat and
/auto_router/test_routing), both pass-through routes,/v1/systemone,/v1/decisions,/model/new,/health,/public/providers/fields,/model/settings, the guardrail decision model list, and main's AI Hub garden on the merged tree. The merge into current main is clean and matched head on 27 of 28 cells. The one difference is main's #45734, where a laya call withimagesnow answers 400 unlessdrop_paramsis setNot verified.
BESPOKE_API_BASE=http://v1was not booted, since bespoke shares laya's connection code. The bespoke Auto Router was left out of the second run because Ollama took about 4 minutes per call under load, and it matched the first run's key differenceType
🆕 New Feature
Caveats (if any)
Low
laya/*andbespoke/*cost $0 unless the deployment sets Custom Pricing, so their spend rows show real tokens and $0. The $0 rows are main's, likeollama/*api_basenames theLAYA_API_BASEorBESPOKE_API_BASEserver now sends that server's env key. Main sent it only whenapi_basewas left unset, and docs(decisions): add Laya and Bespoke Nimble as decisions providers litellm-docs#2356 updates the docsx-litellm-response-cost-*breakdown headers but no barex-litellm-response-cost, on main too/v1/decisionsignoresstreamandcustom_bodyfor every decisions provider, on main too, and this PR does not touch that routelaya/autoor an Ollama tag likenimble:latestanswers 400, since the allowlist matches the cost-map rowsbespoke/nimble-latestandbespoke/bespokelabs/Bespoke-Nimble-9Bdid not run live, since the QA box has no server for those names. They take the same code path asbespoke/nimble, which ranv1(http://v1) gets 404 for every Decisions provider, Strands included, because main's shared URL builder strips/v1from the whole string. Laya and Bespoke inherit it, while their classifiers and pass-through routes reach such a host/model/newsaves a bad checkpoint or API base, and the 400 shows at the first call, the same as for any provider on mainlayaandbespokerows of the decisions wire integration test run only in main's CircleCI, not in this PR's checksDecisions - /v1/decisions, which main added, so this PR adds no separate evaluation modeFinal Attestation
The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR
d4d74f0 passes /live-pr-risk
Note
Medium Risk
Changes how env API keys are paired with API bases for OSS Decisions classifiers and adds new routing paths, though behavior is heavily tested and keys are not sent to arbitrary deployment URLs.
Overview
Adds Laya and Bespoke Nimble as first-class Decisions providers so
laya/*andbespoke/*models can be configured in the proxy Add Model form and routed to<api_base>/v1/systemonewith allowlisted checkpoints and optional API keys.Decisions connection setup now goes through
resolve_connectiononBaseDecisionsConfig(replacing separate base/key resolution indecisions/call.py). Laya and Bespoke use sharedoss_connectionlogic that normalizes API bases (including stripping a trailing/v1so requests do not hit/v1/v1/systemone) and only attachesLAYA_API_KEY/BESPOKE_API_KEYfrom the environment when the target server matches the configured env base—explicit deployment bases elsewhere do not receive the operator env key unless a key is passed in the request.Provider registration, lazy imports, proxy
provider_create_fields.json, and dashboard provider maps/placeholders are updated; integration and unit tests cover proxy dispatch, env-key scoping, and validation of unknown models and malformed API bases.Reviewed by Cursor Bugbot for commit d4d74f0. Bugbot is set up for automated code reviews on this repo. Configure here.