Repository navigation
feat(decisions): add the OpenAI Decisions spec types and the System One translation - #45129
Conversation
|
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. |
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
…ne translation Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…l and usage on responses Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…jectBase Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
a461ae4 to
7e20e92
Compare
…vented min_length bounds Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
… read the translator top down Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…uest and response envelopes Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
… 3.10 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
Reviewing and landing this stack bottom up now. Please hold further pushes or rebases on the four branches until it is on main |
|
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 7a175a5. Configure here.
Takes the reviewed Decisions types from #45129 as the one litellm/types/decisions.py module (openai_decisions.py is folded into it), keeps the OpenAI-shaped /v1/decisions body and the System One translation from this branch, and drops the empty-collection 400s so an empty questions, choices or levels list is left for the provider to judge, as main's types do
…ne translation (BerriAI#45129) * feat(decisions): add the OpenAI Decisions spec types and the System One translation Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * refactor(decisions): share one DecisionsModel config and require model and usage on responses Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * refactor(decisions): rename the shared pydantic parent to DecisionsObjectBase Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(decisions): match the SDK on strict choice values and drop the invented min_length bounds Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * refactor(decisions): compose DecisionsRequest from model and body and read the translator top down Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * refactor(decisions): use the plural Decisions prefix only for the request and response envelopes Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(decisions): import assert_never from typing_extensions for Python 3.10 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 79f62db)
TLDR
Problem this solves:
/v1/decisionsuses the TypeSafe/Jev System One body, not OpenAI's Decisions specHow it solves it:
litellm/types/decisions.py's future replacement aslitellm/types/openai_decisions.py: the OpenAI Decisions request and response modelslitellm/llms/base_llm/decisions/systemone.py: OpenAI <-> Jev System One translation, no callers yetPR 1 of 4 for LIT-9282. Nothing in this PR is imported by the endpoint, so
/v1/decisionsbehaves exactly as on the default branch. Review question for this PR is only "do the models match https://developers.openai.com/api/docs/guides/decisions and is the translation right". PR 2 migrates the 5 Jev providers onto provider configs, PR 3 switches/v1/decisionsto these types, PR 4 addsopenai/gpt-6-lunaModel shape, checked against the generated types in
openai-python(decision_create_params.py,decision.py): every request-side optional (questions[].name,choices[].description,levels[].description,safety_identifier, messagetype, imagedetail) and the answernameare optional in the spec and stay optional here. Response-sidemodel,usageand everyusagecounter and*_detailsobject are required in the spec and are required here too, so a reply missing them fails validation instead of defaulting toNoneor0. The translator fills all of them for Jev providers:total_tokensis the sum, details are zero, and a Jev reply with nomodelreports the requested model. The Jev wire types (SystemOneResponse.model/.usage) stay optional because some Jev providers omit themAll 14 pydantic classes share one parent,
DecisionsModel(LiteLLMPydanticObjectBase), which carriesextra="allow", frozen=Trueonce;LiteLLMPydanticObjectBasestays underneath forprotected_namespaces=()and the deferred-build handling the repo relies onWhat the translator does, for a Jev provider:
inputstring becomesstate(messages are flattened to text),predicatebecomesnoul,choicesbecome thecriteriamap,levelsbecome thecriterialist, the orderedquestionsarray gets map keys (the questionname, or a positionalq0style key when unnamed), and the Jevanswersmap comes back as an ordered OpenAIanswersarray withtotal_tokensfilled in. Things Jev cannot carry (input_image, boolean or repeated choicevalues, duplicatenames) raise a 400 instead of being droppedUser Flow
Before: a developer calling
/v1/decisionswith the Jev body gets answers in the Jev shapetypesafe/jev-1.13.0tomodel_listwithmodel_info.mode: evaluationand start the proxy"state", aquestionsmap and anoulquestionanswerskeyed by question name (noul,choice,score) andusagewith input and output tokensAfter: the same request answers exactly the same, since nothing in this PR is reachable from the endpoint yet
typesafe/jev-1.13.0tomodel_listwithmodel_info.mode: evaluationand start the proxy"state", aquestionsmap and anoulquestionanswersandusage; an OpenAI-shaped body still gets 400 until PR 3 landsLinear ticket
Resolves LIT-9282
Pre-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
Nothing in this PR is reachable from the endpoint (the only imports of the two new modules are their own tests), so there is no before=fail/after=succeeds leg to run. The proof is that
/v1/decisionsand the baseline routes answer the same at the merge base and at the tip, and that the outbound TypeSafe request is identical. Two live proxies on the QA box, each--num_workers 2with its own Postgres (--use_prisma_db_push), real TypeSafe and OpenAI calls, curl as the end-user clientConfig, the same on both legs (
WIRE_BASEis that leg's recorder, which logs each request and forwards it unchanged):body_jev.json(the Jev body the endpoint takes today):{ "model": "typesafe/jev-1.13.0", "state": "Ticket (billing): The export job hangs at 99% and never finishes", "questions": { "defect": {"type": "noul", "instructions": "Is this a defect?"}, "severity": {"type": "choice", "instructions": "How severe is it?", "criteria": {"low": "Cosmetic", "high": "Blocks the user"}}, "confidence": {"type": "score", "instructions": "How confident are you?", "criteria": ["Not sure", "Certain"]} } }body_openai.json(an OpenAI-style body:inputmessages and aquestionslist):{ "model": "typesafe/jev-1.13.0", "input": [{"role": "user", "content": [{"type": "input_text", "text": "Ticket (billing): The export job hangs at 99% and never finishes"}]}], "questions": [ {"type": "predicate", "name": "defect", "instructions": "Is this a defect?"}, {"type": "choice", "name": "severity", "instructions": "How severe is it?", "choices": {"low": "Cosmetic", "high": "Blocks the user"}} ] }Cells, run against each leg with
PROXYandKEY(the master key) set; every JSON reply is printed throughjq -cand cut at 700 characters:Before (fa2c898, 2 workers, 127.0.0.1:52903)
After (7a175a5, 2 workers, 127.0.0.1:48395)
Key cache check, both legs
The first run's bad-key cell sent two
Authorizationheaders (the master key andsk-not-a-key), so it is left out above and re-run cleanly here, together with the deleted key called 8 times a second apart (for i in 1 2 3 4 5 6 7 8; do curl -sS -o /dev/null -w '%{http_code} ' -X POST "$PROXY/v1/decisions" -H "Authorization: Bearer $NEWKEY" ... ; sleep 1; done):Outbound wire
Recorder logs normalized with
jq -Sc 'del(.ts, .upstream_ms)'(the recorder redactsauthorization). Every upstream call isPOST /v1/systemonewith the header setaccept, authorization, content-type, user-agentand the same body on both legs. Before logged 8 requests and After 12, the 4 extra being the extra 200s of the deleted-key loop; the 8 shared records diff empty line for line. One record:{"body":{"model":"jev-1.13.0","questions":{"confidence":{"criteria":["Not sure","Certain"],"instructions":"How confident are you?","type":"score"},"defect":{"instructions":"Is this a defect?","type":"noul"},"severity":{"criteria":{"high":"Blocks the user","low":"Cosmetic"},"instructions":"How severe is it?","type":"choice"}},"state":"Ticket (billing): The export job hangs at 99% and never finishes"},"headers":{"accept":"*/*","authorization":"<redacted len=115>","content-type":"application/json","user-agent":"litellm/1.106.0"},"method":"POST","path":"/v1/systemone","status":200}Observations
Verdict: PASS (Before = After at every cell, the only movement being sampling noise in
score)Last updated: 7a175a5 (the proof ran at 7a175a5, the current tip)
Type
🆕 New Feature
Caveats (if any)
Low
litellm/types/decisions.pykeeps the Jev shape at this tipq0,q1); if a caller also names a questionq0, the unnamed one is sent as_q0roleis not forwardedmodelreports the model the caller requested, since the OpenAI response requiresmodelsafety_identifieris accepted on the request model but not forwarded to System One providers, which have no field for it; whether it drops underdrop_paramsand 400s without it is settled in fix(decisions)!: refuse safety_identifier on providers that cannot take it unless drop_params drops it #44955, where the translator gets its callernext16.3.6 inui/litellm-dashboard/package-lock.jsonmatches 6 advisories fixed in 16.3.8 (job); it is repo-wide right now and not a required check, main's last run passed before they published, so it is not caused by this PR/key/deletekeeps answering on the worker that did not serve the delete until its in-memory entry expires (DEFAULT_MANAGEMENT_OBJECT_IN_MEMORY_CACHE_TTL, 60s by default); outside this diff, seen in the proof's key cache checkFinal Attestation
Link to Devin session: https://app.devin.ai/sessions/a7a7d712811643b1a6416531d7d99cfa
Open in Devin Desktop: https://app.devin.ai/desktop/session/a7a7d712811643b1a6416531d7d99cfa?variant=devin
Requested by: @kerry-berri
Note
Low Risk
Additive, unwired types and a translator with no runtime path changes; endpoint behavior stays the same until follow-up PRs.
Overview
Introduces OpenAI Decisions Pydantic models in
litellm/types/openai_decisions.py(requestinput/questions, responseanswers/usagewith required token detail fields, plushidden_paramsoff the wire). Addssystemone.pyto translate between that shape and the Jev/System One wire format (state, map-keyedquestions/answers,noul/criteria) for TypeSafe-style providers.Translation covers predicate→
noul, choice/score→criteria, flattening messageinputtostate, positional keys for unnamed questions, and building ordered OpenAI answers with filledtotal_tokensand zeroed detail fields. Unsupported Jev cases (input_image, boolean/repeated choices, duplicate names) raise 400; mismatched provider answers raise 500.Unit tests only—nothing imports these modules yet, so
/v1/decisionsandlitellm/types/decisions.pybehavior is unchanged (PR 1 of 4 for LIT-9282).Reviewed by Cursor Bugbot for commit 7a175a5. Bugbot is set up for automated code reviews on this repo. Configure here.