Repository navigation
feat(sdk): add litellm.systemone and trim litellm.decisions to the OpenAI shape - #45846
Conversation
…enAI shape 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)".
|
|
kerry seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account. You have signed the CLA already but the status is still pending? Let us recheck it. |
|
| model=provider.model, state=_STATE, questions=_SDK_QUESTIONS, api_base=handle.api_base(), api_key=_API_KEY | ||
| ) | ||
| asynchronous: Final = await litellm.adecisions( | ||
| asynchronous: Final = await litellm.asystemone( |
There was a problem hiding this comment.
The sync side still calls litellm.decisions(..., state=...), which now raises BadRequestError before reaching the upstream. Renaming only the async call leaves this test broken. The connection-error test and the signed Strands test also retain the old sync call.
Rename those remaining sync calls to litellm.systemone so the tests reach the upstream and check the intended behavior.
| row["api_base"], | ||
| _number(row["spend"]), | ||
| ) == ("success", "adecisions", "databricks", deployment, f"{handle.api_base()}/{_ENDPOINT}/invocations", 0.0), ( | ||
| ) == ("success", "asystemone", "databricks", deployment, f"{handle.api_base()}/{_ENDPOINT}/invocations", 0.0), ( |
There was a problem hiding this comment.
This test posts to /v1/decisions, which still logs adecisions, but the changed assertion expects asystemone. It will fail even when the proxy records the correct call type. The Strands test makes the same mistake by expecting asystemone for all four routes, including /decisions and /v1/decisions.
Keep adecisions here and choose the Strands expectation by route.
Merging this PR will not alter performance
|
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…onWarning Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…UserWarning Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ers to litellm.decisions.call The decision model guardrail backport (#45894) landed on the line after this branch forked and imports supported_decisions_providers from litellm.decisions.main, which #45846 replaces here. Match main by exposing it from litellm.decisions.call and point the guardrail endpoint and its test at that module. schema.d.ts and the lazy OpenAPI snapshot are regenerated on the merged tree Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…enAI shape (BerriAI#45846) * feat(sdk): add litellm.systemone and trim litellm.decisions to the OpenAI shape Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * ci: run tests/unit/systemone in the core-utils unit shard Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * feat(sdk): keep litellm.decisions(state=...) working with a DeprecationWarning Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test: make tests/unit/systemone a package Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * feat(sdk): surface the litellm.decisions(state=...) deprecation as a UserWarning Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: kerry <kerry@berri.ai> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> (cherry picked from commit 25d9b05)
TLDR
Problem this solves:
litellm.decisionsaccepted both the OpenAI Decisions shape and the System One shape/v1/systemonetraffic was logged as call typeadecisions, indistinguishable from/v1/decisionsHow it solves it:
litellm.systemone/litellm.asystemonetakestateplus aquestionsmappinglitellm.decisions/litellm.adecisionsdocument onlyinput, aquestionslist,safety_identifierlitellm.decisions(state=...)keeps working for existing Jev callers, it sends the System One format and emits aUserWarningpointing atlitellm.systemonelitellm.systemone(input=...)raisesBadRequestErrorpointing atlitellm.decisionslitellm/decisions/call.pyCallTypes.systemone/asystemone,router.systemone/router.asystemone,/v1/systemoneruns asasystemonelitellm.asystemoneIntentional product change: spend logs and the Logs page show
asystemonefor/v1/systemonerequests instead ofadecisionsUser Flow
Before: a developer sending the System One shape through the SDK has to pass it to
litellm.decisions, and a wrong field mix gets a pydantic validation dumplitellm.decisions(model="typesafe/jev-latest", state="...", questions={"is_refund_request": {...}})and get the System One response backlitellm.systemone(...)and getAttributeError: module 'litellm' has no attribute 'systemone'statewith an OpenAI question list and getInvalid Decisions request: 1 validation error for DecisionsRequestBody .../v1/systemoneon the proxy and https://litellm-domain/ui/?page=logs shows the row asadecisionsAfter: the same developer picks the function by the request shape they write
litellm.systemone(model="typesafe/jev-latest", state="...", questions={"is_refund_request": {...}})and get the System One response backlitellm.decisions(model=..., input="...", questions=[{...}])keeps returning the OpenAI response unchangedlitellm.decisions(state=...)still returns the System One answers and printsUserWarning: litellm.decisions(state=...) is deprecated, call litellm.systemone for the System One request format, andlitellm.systemone(input=...)raisesBadRequestErrorpointing atlitellm.decisions/v1/systemoneon the proxy and https://litellm-domain/ui/?page=logs shows the row asasystemone, while/v1/decisionsrows stayadecisionsPre-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 more@greptileaito re-request a review after pushing changes)Delays 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
Setup:
source /tmp/qa.envexportsTYPESAFE_API_KEY(1Password Shared vault),OPENAI_API_KEYfrom the environment. Proxy config at/tmp/proof_config.yaml:Proxy started with
python litellm/proxy/proxy_cli.py --config /tmp/proof_config.yaml --port 4000 --use_v2_migration_resolverBefore (290ac3d, main at capture time)
SDK: litellm.systemone
python -c 'import litellm; litellm.systemone(model="typesafe/jev-latest", state="Customer wrote: I was charged twice for order #4411 and want a refund today.", questions={"is_refund_request": {"type": "noul", "instructions": "Is the customer asking for a refund?"}})'AttributeError: module 'litellm' has no attribute 'systemone'SDK: litellm.decisions with state
python -c 'import litellm; litellm.decisions(model="openai/gpt-6-luna", state="hello", questions=[{"type": "predicate", "name": "q", "instructions": "x"}])'BadRequestError 400 litellm.BadRequestError: Invalid Decisions request: 1 validation error for DecisionsRequestBody(a pydantic dump, nothing tells the caller which function to use)Proxy: /v1/systemone call type in spend logs
curl -s -D /tmp/h localhost:4000/v1/systemone -H "Authorization: Bearer sk-proof-1234" -H "Content-Type: application/json" -d '{"model":"jev-latest","state":"Customer wrote: I was charged twice for order #4411 and want a refund today.","questions":{"is_refund_request":{"type":"noul","instructions":"Is the customer asking for a refund?"}}}'{"model":"jev-1.13.0","answers":{"is_refund_request":{"type":"noul","noul":0.99}},"usage":{"input_tokens":294,"output_tokens":23}}curl -s "localhost:4000/spend/logs?request_id=$(grep -i ^x-litellm-call-id /tmp/h | awk '{print $2}' | tr -d '\r')" -H "Authorization: Bearer sk-proof-1234"{'call_type': 'adecisions', 'model': 'typesafe/jev-latest', 'spend': 1.2348e-05, 'status': 'success'}After (a95b48a)
SDK: litellm.systemone
systemone -> {'type': 'noul', 'noul': 0.99} {'input_tokens': 294, 'output_tokens': 23}SDK: litellm.decisions with state (backwards compatible)
UserWarningshows by defaultdecisions(state=) -> {'type': 'noul', 'noul': 0.99} | warning: ['litellm.decisions(state=...) is deprecated, call litellm.systemone for the System One request format']adecisions(state=) -> {'type': 'noul', 'noul': 0.99} | warning: ['litellm.decisions(state=...) is deprecated, call litellm.systemone for the System One request format']SDK: litellm.decisions with input (unchanged)
python -c 'import litellm; r = litellm.decisions(model="openai/gpt-6-luna", input="Customer wrote: I was charged twice for order #4411 and want a refund today.", questions=[{"type": "predicate", "name": "is_refund_request", "instructions": "Is the customer asking for a refund?"}]); print(r.answers[0].model_dump(mode="json"))'{'type': 'predicate', 'name': 'is_refund_request', 'probability': 1.0}Proxy: /v1/systemone call type in spend logs
{"model":"jev-1.13.0","answers":{"is_refund_request":{"type":"noul","noul":0.99}},"usage":{"input_tokens":294,"output_tokens":23}}{'call_type': 'asystemone', 'model': 'typesafe/jev-latest', 'spend': 1.2348e-05, 'status': 'success'}Proxy: /v1/decisions call type unchanged
curl -s -D /tmp/h2 localhost:4000/v1/decisions -H "Authorization: Bearer sk-proof-1234" -H "Content-Type: application/json" -d '{"model":"jev-latest","input":"Customer wrote: I was charged twice for order #4411 and want a refund today.","questions":[{"type":"predicate","name":"is_refund_request","instructions":"Is the customer asking for a refund?"}]}'{"model":"jev-1.13.0","answers":[{"type":"predicate","name":"is_refund_request","probability":0.99}],"usage":{"input_tokens":294,...,"total_tokens":317}}{'call_type': 'adecisions', 'model': 'typesafe/jev-latest', 'spend': 1.2348e-05, 'status': 'success'}Provider matrix at a95b48a, both request shapes against every provider we hold a key for (TypeSafe, OpenAI, Databricks, OpenRouter, Perplexity, Cloudflare)
Same four calls per provider,
litellm.systemone(state=..., questions={...})andlitellm.decisions(input=..., questions=[...]), state "Hi, I want my money back for order 1234.", oneis_refund_requestquestion. Keys from the Shared 1Password vault, real spendtypesafe/jev-latest{'type': 'noul', 'noul': 0.99}{'type': 'predicate', 'name': 'is_refund_request', 'probability': 0.99}openai/gpt-6-luna{'type': 'noul', 'noul': 1.0}{'type': 'predicate', 'name': 'is_refund_request', 'probability': 1.0}databricks/databricks-openjev-qwen35-4b{'type': 'noul', 'noul': 0.8807970842359877}{'type': 'predicate', 'name': 'is_refund_request', 'probability': 0.8807970842359877}openrouter/typesafe/jev-1.13{'type': 'noul', 'noul': 0.99}{'type': 'predicate', 'name': 'is_refund_request', 'probability': 0.99}perplexity/pplx-decider-v1-27b{'type': 'noul', 'noul': 0.9997092001918163}{'type': 'predicate', 'name': 'is_refund_request', 'probability': 0.9997092001918163}cloudflare/clef{'type': 'noul', 'noul': 0.9944}{'type': 'predicate', 'name': 'is_refund_request', 'probability': 0.9944}azure_ai/Microsoft-Decision-1DeploymentNotFoundon all 4 Foundry resources in the vault, no Decision-1 deployment exists to test againststrands_decider/...(Bedrock AgentCore)So the System One shape reaches OpenAI (
systemone->openai/gpt-6-luna) and the OpenAI shape reaches Jev (decisions->typesafe/jev-latest) through the shared IR, and each provider's own shape round-trips tooType
🆕 New Feature
🧹 Refactoring
Caveats (if any)
Severe
litellm.decisions(state=...)now emits aUserWarningon every call, callers rename tolitellm.systemoneto silence it/v1/systemonecall type changes fromadecisionstoasystemonein spend logs and the Logs pageLow
# pyright: ignoreon the newrouter.factory_functioncall sites, same untyped factory every sibling usesFinal Attestation
Link to Devin session: https://app.devin.ai/sessions/7fa4332c8dd6470c89614432409b9174
Open in Devin Desktop: https://app.devin.ai/desktop/session/7fa4332c8dd6470c89614432409b9174?variant=devin
Requested by: @kerry-berri
Note
Devin errored when opening this Pull Request as kerry-berri.
As a fallback, Devin opened this PR as itself.