Skip to content

feat(decisions): add the OpenAI Decisions spec types and the System One translation - #45129

Merged
mateo-berri merged 7 commits into
mainfrom
litellm_decisions_openai_spec
Oct 8, 2026
Merged

mateo-berri merged 7 commits into
mainfrom
litellm_decisions_openai_spec

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • /v1/decisions uses the TypeSafe/Jev System One body, not OpenAI's Decisions spec
  • Switching the public schema in one PR is too big to review

How it solves it:

  • Adds litellm/types/decisions.py's future replacement as litellm/types/openai_decisions.py: the OpenAI Decisions request and response models
  • Adds litellm/llms/base_llm/decisions/systemone.py: OpenAI <-> Jev System One translation, no callers yet
  • Unit tests for both, nothing else changes

PR 1 of 4 for LIT-9282. Nothing in this PR is imported by the endpoint, so /v1/decisions behaves 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/decisions to these types, PR 4 adds openai/gpt-6-luna

Model 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, message type, image detail) and the answer name are optional in the spec and stay optional here. Response-side model, usage and every usage counter and *_details object are required in the spec and are required here too, so a reply missing them fails validation instead of defaulting to None or 0. The translator fills all of them for Jev providers: total_tokens is the sum, details are zero, and a Jev reply with no model reports the requested model. The Jev wire types (SystemOneResponse.model / .usage) stay optional because some Jev providers omit them

All 14 pydantic classes share one parent, DecisionsModel(LiteLLMPydanticObjectBase), which carries extra="allow", frozen=True once; LiteLLMPydanticObjectBase stays underneath for protected_namespaces=() and the deferred-build handling the repo relies on

What the translator does, for a Jev provider: input string becomes state (messages are flattened to text), predicate becomes noul, choices become the criteria map, levels become the criteria list, the ordered questions array gets map keys (the question name, or a positional q0 style key when unnamed), and the Jev answers map comes back as an ordered OpenAI answers array with total_tokens filled in. Things Jev cannot carry (input_image, boolean or repeated choice values, duplicate names) raise a 400 instead of being dropped

User Flow

Before: a developer calling /v1/decisions with the Jev body gets answers in the Jev shape

  1. They add typesafe/jev-1.13.0 to model_list with model_info.mode: evaluation and start the proxy
  2. They send POST http://localhost:4000/v1/decisions with "state", a questions map and a noul question
  3. They get 200 with answers keyed by question name (noul, choice, score) and usage with input and output tokens

After: the same request answers exactly the same, since nothing in this PR is reachable from the endpoint yet

  1. They add typesafe/jev-1.13.0 to model_list with model_info.mode: evaluation and start the proxy
  2. They send the same POST http://localhost:4000/v1/decisions with "state", a questions map and a noul question
  3. They get the same 200 with the same Jev-shaped answers and usage; an OpenAI-shaped body still gets 400 until PR 3 lands

Linear ticket

Resolves LIT-9282

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. 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
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to 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/decisions and 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 2 with its own Postgres (--use_prisma_db_push), real TypeSafe and OpenAI calls, curl as the end-user client

Leg Commit Proxy Recorder in front of https://api.typesafe.ai
Before fa2c898 (merge base) 127.0.0.1:52903 127.0.0.1:16758
After 7a175a5 (tip) 127.0.0.1:48395 127.0.0.1:16759

Config, the same on both legs (WIRE_BASE is that leg's recorder, which logs each request and forwards it unchanged):

model_list:
  - model_name: typesafe/jev-1.13.0
    litellm_params:
      model: typesafe/jev-1.13.0
      api_key: os.environ/TYPESAFE_API_KEY
      api_base: os.environ/WIRE_BASE
    model_info:
      mode: evaluation
  - model_name: gpt-5-mini
    litellm_params:
      model: openai/gpt-5-mini
      api_key: os.environ/OPENAI_API_KEY
general_settings:
  master_key: os.environ/LITELLM_MASTER_KEY

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: input messages and a questions list):

{
  "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 PROXY and KEY (the master key) set; every JSON reply is printed through jq -c and cut at 700 characters:

curl -sS -w '\nHTTP %{http_code}\n' "$PROXY/health/liveliness" -H "Authorization: Bearer $KEY"
curl -sS -w '\nHTTP %{http_code}\n' "$PROXY/health/readiness" -H "Authorization: Bearer $KEY"
curl -sS -w '\nHTTP %{http_code}\n' -X POST "$PROXY/v1/decisions" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d @body_jev.json
curl -sS -w '\nHTTP %{http_code}\n' -X POST "$PROXY/v1/decisions" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d @body_openai.json
curl -sS -w '\nHTTP %{http_code}\n' -X POST "$PROXY/v1/decisions" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{"model":"nosuch/jev-1.13.0","state":"x","questions":{"q":{"type":"noul","instructions":"ok?"}}}'
curl -sS -w '\nHTTP %{http_code}\n' -X POST "$PROXY/v1/chat/completions" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{"model":"gpt-5-mini","messages":[{"role":"user","content":"Reply with the single word pong"}],"max_completion_tokens":16}'
curl -sS -N -X POST "$PROXY/v1/chat/completions" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{"model":"gpt-5-mini","messages":[{"role":"user","content":"Count from 1 to 5, digits only"}],"stream":true,"max_completion_tokens":32}'
curl -sS -w '\nHTTP %{http_code}\n' -X POST "$PROXY/v1/messages" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{"model":"gpt-5-mini","max_tokens":16,"messages":[{"role":"user","content":"Reply with the single word pong"}]}'
NEWKEY=$(curl -sS -X POST "$PROXY/key/generate" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{"models":["typesafe/jev-1.13.0"],"key_alias":"lit9282"}' | jq -r .key)
curl -sS -w '\nHTTP %{http_code}\n' -X POST "$PROXY/v1/decisions" -H "Authorization: Bearer $NEWKEY" -H 'Content-Type: application/json' -d @body_jev.json
curl -sS -w '\nHTTP %{http_code}\n' "$PROXY/key/info?key=$NEWKEY" -H "Authorization: Bearer $KEY"
curl -sS -w '\nHTTP %{http_code}\n' -X POST "$PROXY/key/delete" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{"keys":["'"$NEWKEY"'"]}'
curl -sS -o /dev/null -w '%{http_code}\n' -X POST "$PROXY/v1/decisions" -H "Authorization: Bearer $NEWKEY" -H 'Content-Type: application/json' -d @body_jev.json
sleep 12; curl -sS -w '\nHTTP %{http_code}\n' "$PROXY/spend/logs" -H "Authorization: Bearer $KEY"

Before (fa2c898, 2 workers, 127.0.0.1:52903)

== health_liveliness [base] HTTP 200 time=0.002390s
"I'm alive!"
== health_readiness [base] HTTP 200 time=0.005985s
{"status":"healthy","db":"connected"}
== decisions_jev [base] HTTP 200 time=0.399521s
{"model":"jev-1.13.0","answers":{"defect":{"type":"noul","noul":0.86},"severity":{"type":"choice","choice":"high","confidence":1.0,"probabilities":{"low":0.0,"high":1.0}},"confidence":{"type":"score","score":0.48,"confidence":0.04,"legend":{"0":"Not sure","1":"Certain"},"probabilities":{"0":0.52,"1":0.48}}},"usage":{"input_tokens":368,"output_tokens":62}}
== decisions_openai_shape [base] HTTP 400 time=0.093817s
{"error":{"message":"litellm.BadRequestError: Invalid Decisions request: 2 validation errors for DecisionsRequestBody\nstate\n  Field required [type=missing, input_value={'model': 'typesafe/jev-1...': 'Blocks the user'}}]}, input_type=dict]\n    For further information visit https://errors.pydantic.dev/2.13/v/missing\nquestions\n  Input should be a valid dictionary [type=dict_type, input_value=[{'type': 'predicate', 'n...h': 'Blocks the user'}}], input_type=list]\n    For further information visit https://errors.pydantic.dev/2.13/v/dict_type","type":"invalid_request_error","param":null,"code":"400"}}
== decisions_unknown_provider [base] HTTP 400 time=0.044339s
{"error":{"message":"/decisions: Invalid model name passed in model=nosuch/jev-1.13.0. Call `/v1/models` to view available models for your key.","type":"invalid_request_error","param":null,"code":"400","provider_specific_fields":{"error":"/decisions: Invalid model name passed in model=nosuch/jev-1.13.0. Call `/v1/models` to view available models for your key."}}}
== chat_nonstream [base] HTTP 200 time=1.171801s
{"id":"chatcmpl-EWWJHhNswRzrdNss7CvoHYKIungVt","created":1791419403,"model":"gpt-5-mini","object":"chat.completion","choices":[{"finish_reason":"length","index":0,"message":{"content":"","role":"assistant","provider_specific_fields":{"refusal":null},"annotations":[]},"provider_specific_fields":{}}],"usage":{"completion_tokens":16,"prompt_tokens":12,"total_tokens":28,"completion_tokens_details":{"accepted_prediction_tokens":0,"audio_tokens":0,"reasoning_tokens":16,"rejected_prediction_tokens":0},"prompt_tokens_details":{"audio_tokens":0,"cached_tokens":0}},"service_tier":"default"}
== chat_stream [base]
chunks=3 done=1 text=
== messages [base] HTTP 200 time=0.905197s
{"id":"resp_bGl0ZWxsbTpjdXN0b21fbGxtX3Byb3ZpZGVyOm9wZW5haTttb2RlbF9pZDplMGFkYzFkODc1ZDA2MjAxMDUyMTNiMDBhYjU5ZjE1NDJjODQ5ZThlM2UxZmM0NDgwOWNjMDNmMzJmYjFjOTJjO3Jlc3BvbnNlX2lkOnJlc3BfMDc0MDU5MjAyMGZjNTRkYzAwNmFjNmU0MGQ3ODEwODdkMTkzYzk1MDk2N2Y3ZTA0ZmM=","type":"message","role":"assistant","model":"gpt-5-mini","stop_sequence":null,"usage":{"input_tokens":12,"output_tokens":0},"content":[{"type":"redacted_thinking","data":"litellm_encrypted_reasoning:gAAAAABqxuQOzMokh3uk6vRwM2d4eTv26JIGRQQ6f6DTT0Y0iSw1KDWKUAu1GG0UzwbIbrDGeQYR9DaBLQoRlJzaLaoKFRBYyNCMuyBQbckz-iR1TabwBeQF06o81ROUuElA3_vl-I7Xhx8GgiZ_vggmmfp0tgaa7MFauxYmZq8CF-DVGsJ4RkjOwJaQWxgKjYh58_MWX-2X7q9gfwAEFwMdJyUPUS2DB1XCvwR9G6iVFPml__VH2LUmK5c
== key_generate [base] HTTP 200 time=0.059234s
{"key_alias":"lit9282-base","duration":null,"models":["typesafe/jev-1.13.0"],"spend":0.0,"max_budget":null,"user_id":null,"team_id":null,"agent_id":null,"max_parallel_requests":null,"metadata":{},"tpm_limit":null,"rpm_limit":null,"budget_duration":null,"budget_limits":null,"allowed_cache_controls":[],"config":{},"permissions":{},"model_max_budget":{},"budget_fallbacks":{},"model_rpm_limit":null,"model_tpm_limit":null,"mcp_rpm_limit":null,"tag_rpm_limit":null,"guardrails":null,"policies":null,"prompts":null,"blocked":null,"aliases":{},"object_permission":null,"key":"sk-<redacted>","tpd_limit":null,"default_estimated_output_tokens":null,"default_estimated_output_tokens_per_model":n
== decisions_with_generated_key [base] HTTP 200
{"model":"jev-1.13.0","answer_types":["noul","choice","score"],"usage":{"input_tokens":368,"output_tokens":62}}
== key_info [base] HTTP 200 time=0.011876s
{"key":"sk-<redacted>","info":{"key_name":"sk-...taBQ","key_alias":"lit9282-base","soft_budget_cooldown":false,"spend":0.0,"total_spend":0.0,"expires":null,"models":["typesafe/jev-1.13.0"],"aliases":{},"config":{},"router_settings":{},"user_id":null,"team_id":null,"agent_id":null,"project_id":null,"permissions":{},"max_parallel_requests":null,"metadata":{},"blocked":null,"tpm_limit":null,"rpm_limit":null,"tpd_limit":null,"max_budget":null,"budget_duration":null,"budget_reset_at":null,"allowed_cache_controls":[],"allowed_routes":[],"key_type":"default","policies":[],"access_group_ids":[],"model_spend":{},"model_max_budget":{},"budget_fallbacks":{},"budget_id":null,"organization_id
== key_delete [base] HTTP 200 time=0.041056s
{"deleted_keys":["sk-<redacted>"]}
== decisions_after_delete [base] HTTP 401
== spend_logs [base] HTTP 200 time=0.026191s
[{"request_id":"319893cf-3377-4213-9d72-5fca489de356","call_type":"adecisions","api_key":"77af0e26e92bc556e7bf7f39dc5658b7e03f05f55c8ec8bcd369313208db1782","spend":0.0,"total_tokens":0,"prompt_tokens":0,"completion_tokens":0,"startTime":"2026-10-08T00:30:06.610000Z","endTime":"2026-10-08T00:30:06.619000Z","request_duration_ms":9,"completionStartTime":"2026-10-08T00:30:06.619000Z","model":"typesafe/jev-1.13.0","model_id":"88c3f01f1ba09d2e16fcf100c7fef1c08ccf4875fa5469ac679582d10ecc3f8c","model_group":"typesafe/jev-1.13.0","custom_llm_provider":"typesafe","api_base":"","user":"","metadata":{"status":"failure","user_agent":"curl/8.5.0","max_retries":null,"batch_models":null,"usage_object":{"tot
[{"model":"typesafe/jev-1.13.0","call_type":"adecisions","spend":0.0,"total_tokens":0,"status":"failure"},{"model":"typesafe/jev-1.13.0","call_type":"adecisions","spend":0.000015456,"total_tokens":430,"status":"success"},{"model":"typesafe/jev-1.13.0","call_type":"adecisions","spend":0.000015456,"total_tokens":430,"status":"success"},{"model":"unknown-model","call_type":"adecisions","spend":0.0,"total_tokens":0,"status":"failure"},{"model":"typesafe/jev-1.13.0","call_type":"","spend":0.0,"total_tokens":0,"status":"failure"},{"model":"typesafe/jev-1.13.0","call_type":"adecisions","spend":0.0000

After (7a175a5, 2 workers, 127.0.0.1:48395)

== health_liveliness [head] HTTP 200 time=0.001959s
"I'm alive!"
== health_readiness [head] HTTP 200 time=0.019974s
{"status":"healthy","db":"connected"}
== decisions_jev [head] HTTP 200 time=0.343902s
{"model":"jev-1.13.0","answers":{"defect":{"type":"noul","noul":0.87},"severity":{"type":"choice","choice":"high","confidence":1.0,"probabilities":{"low":0.0,"high":1.0}},"confidence":{"type":"score","score":0.41,"confidence":0.18,"legend":{"0":"Not sure","1":"Certain"},"probabilities":{"0":0.59,"1":0.41}}},"usage":{"input_tokens":368,"output_tokens":62}}
== decisions_openai_shape [head] HTTP 400 time=0.021251s
{"error":{"message":"litellm.BadRequestError: Invalid Decisions request: 2 validation errors for DecisionsRequestBody\nstate\n  Field required [type=missing, input_value={'model': 'typesafe/jev-1...': 'Blocks the user'}}]}, input_type=dict]\n    For further information visit https://errors.pydantic.dev/2.13/v/missing\nquestions\n  Input should be a valid dictionary [type=dict_type, input_value=[{'type': 'predicate', 'n...h': 'Blocks the user'}}], input_type=list]\n    For further information visit https://errors.pydantic.dev/2.13/v/dict_type","type":"invalid_request_error","param":null,"code":"400"}}
== decisions_unknown_provider [head] HTTP 400 time=0.096418s
{"error":{"message":"/decisions: Invalid model name passed in model=nosuch/jev-1.13.0. Call `/v1/models` to view available models for your key.","type":"invalid_request_error","param":null,"code":"400","provider_specific_fields":{"error":"/decisions: Invalid model name passed in model=nosuch/jev-1.13.0. Call `/v1/models` to view available models for your key."}}}
== chat_nonstream [head] HTTP 200 time=1.046677s
{"id":"chatcmpl-EWWJaXKoAXSL3moB50DHo3ckm80wA","created":1791419422,"model":"gpt-5-mini","object":"chat.completion","choices":[{"finish_reason":"length","index":0,"message":{"content":"","role":"assistant","provider_specific_fields":{"refusal":null},"annotations":[]},"provider_specific_fields":{}}],"usage":{"completion_tokens":16,"prompt_tokens":12,"total_tokens":28,"completion_tokens_details":{"accepted_prediction_tokens":0,"audio_tokens":0,"reasoning_tokens":16,"rejected_prediction_tokens":0},"prompt_tokens_details":{"audio_tokens":0,"cached_tokens":0}},"service_tier":"default"}
== chat_stream [head]
chunks=3 done=1 text=
== messages [head] HTTP 200 time=0.892706s
{"id":"resp_bGl0ZWxsbTpjdXN0b21fbGxtX3Byb3ZpZGVyOm9wZW5haTttb2RlbF9pZDplMGFkYzFkODc1ZDA2MjAxMDUyMTNiMDBhYjU5ZjE1NDJjODQ5ZThlM2UxZmM0NDgwOWNjMDNmMzJmYjFjOTJjO3Jlc3BvbnNlX2lkOnJlc3BfMDkxMTA1NTFjZDA1ZGQ3NDAwNmFjNmU0MjA0Y2E0ODdkMTgzYmFhYTY3MmU1YTY2MmQ=","type":"message","role":"assistant","model":"gpt-5-mini","stop_sequence":null,"usage":{"input_tokens":12,"output_tokens":0},"content":[{"type":"redacted_thinking","data":"litellm_encrypted_reasoning:gAAAAABqxuQg1kjpI5v9C2feEy17EpU0zCdvQpQEqvhF0Tqei9y5cDFfNs8ZjL8r-UyByrPAvkh3Bbm1kWX_rO-XrCt_hjRSAHm9p8wPrQCDRrS5Q_JqEmP0p_-tgLBICZSQm4foUEOifMqEv9kgPL6Bi47qQIoVOme3JQ_xAa0g1uPOrLbBHtpmPFhwIjVGsXBrRJ1US3k41HPrUGseJw6XWC8oM44t7_hYSQJM3xALuKlWYZs8IHEiDLO
== key_generate [head] HTTP 200 time=0.055762s
{"key_alias":"lit9282-head","duration":null,"models":["typesafe/jev-1.13.0"],"spend":0.0,"max_budget":null,"user_id":null,"team_id":null,"agent_id":null,"max_parallel_requests":null,"metadata":{},"tpm_limit":null,"rpm_limit":null,"budget_duration":null,"budget_limits":null,"allowed_cache_controls":[],"config":{},"permissions":{},"model_max_budget":{},"budget_fallbacks":{},"model_rpm_limit":null,"model_tpm_limit":null,"mcp_rpm_limit":null,"tag_rpm_limit":null,"guardrails":null,"policies":null,"prompts":null,"blocked":null,"aliases":{},"object_permission":null,"key":"sk-<redacted>","tpd_limit":null,"default_estimated_output_tokens":null,"default_estimated_output_tokens_per_model":n
== decisions_with_generated_key [head] HTTP 200
{"model":"jev-1.13.0","answer_types":["noul","choice","score"],"usage":{"input_tokens":368,"output_tokens":62}}
== key_info [head] HTTP 200 time=0.009327s
{"key":"sk-<redacted>","info":{"key_name":"sk-...2FIg","key_alias":"lit9282-head","soft_budget_cooldown":false,"spend":0.0,"total_spend":0.0,"expires":null,"models":["typesafe/jev-1.13.0"],"aliases":{},"config":{},"router_settings":{},"user_id":null,"team_id":null,"agent_id":null,"project_id":null,"permissions":{},"max_parallel_requests":null,"metadata":{},"blocked":null,"tpm_limit":null,"rpm_limit":null,"tpd_limit":null,"max_budget":null,"budget_duration":null,"budget_reset_at":null,"allowed_cache_controls":[],"allowed_routes":[],"key_type":"default","policies":[],"access_group_ids":[],"model_spend":{},"model_max_budget":{},"budget_fallbacks":{},"budget_id":null,"organization_id
== key_delete [head] HTTP 200 time=0.034791s
{"deleted_keys":["sk-<redacted>"]}
== decisions_after_delete [head] HTTP 200
== spend_logs [head] HTTP 200 time=0.021851s
[{"request_id":"97249bea-f707-4f39-8910-2f7e0a5afef3","call_type":"adecisions","api_key":"ce9685138d757f715f2df98b0a0bb3d8103ab991aaf03760c527084f4a482ff6","spend":0.000015456,"total_tokens":430,"prompt_tokens":368,"completion_tokens":62,"startTime":"2026-10-08T00:30:25.479000Z","endTime":"2026-10-08T00:30:25.640000Z","request_duration_ms":161,"completionStartTime":"2026-10-08T00:30:25.640000Z","model":"typesafe/jev-1.13.0","model_id":"047bce42cfe14754b3cfccbd0bf530a422889f5375f82099604d848d46930306","model_group":"typesafe/jev-1.13.0","custom_llm_provider":"typesafe","api_base":"http://127.0.0.1:16759/v1/systemone","user":"","metadata":{"status":null,"user_agent":null,"max_retries":2,"batch
[{"model":"typesafe/jev-1.13.0","call_type":"adecisions","spend":0.000015456,"total_tokens":430,"status":"success"},{"model":"typesafe/jev-1.13.0","call_type":"adecisions","spend":0.000015456,"total_tokens":430,"status":"success"},{"model":"typesafe/jev-1.13.0","call_type":"adecisions","spend":0.000015456,"total_tokens":430,"status":"success"},{"model":"unknown-model","call_type":"adecisions","spend":0.0,"total_tokens":0,"status":"failure"},{"model":"typesafe/jev-1.13.0","call_type":"","spend":0.0,"total_tokens":0,"status":"failure"},{"model":"typesafe/jev-1.13.0","call_type":"adecisions","spe

Key cache check, both legs

The first run's bad-key cell sent two Authorization headers (the master key and sk-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):

== bad_key_only [base] HTTP 401
== use_new_key_twice [base] 200 200
== delete [base] HTTP 200
== after_delete_8_calls [base] 401 200 401 401 200 401 401 200
== bad_key_only [head] HTTP 401
== use_new_key_twice [head] 200 200
== delete [head] HTTP 200
== after_delete_8_calls [head] 401 200 200 200 200 401 200 200

Outbound wire

Recorder logs normalized with jq -Sc 'del(.ts, .upstream_ms)' (the recorder redacts authorization). Every upstream call is POST /v1/systemone with the header set accept, authorization, content-type, user-agent and 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

  • Jev answers match on both legs except the sampled score
  • OpenAI-style body gets the same 400 on both legs
  • Unknown provider gets the same 400 on both legs
  • Bad key alone gets 401 on both legs
  • Chat, streaming and /v1/messages unchanged on both legs
  • Spend rows identical: adecisions, 430 tokens, 0.000015456
  • Outbound /v1/systemone request identical after normalization
  • Deleted key keeps answering on the other worker, both legs

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)

  • 7a175a5 passes /audit (skipped: no product code on a request path, two new modules with no callers)

Type

🆕 New Feature

Caveats (if any)

Low

  • Dead code until PR 3 wires it in; litellm/types/decisions.py keeps the Jev shape at this tip
  • Unnamed questions get positional Jev keys (q0, q1); if a caller also names a question q0, the unnamed one is sent as _q0
  • OpenAI message input is flattened to text for Jev providers, so role is not forwarded
  • A Jev reply without model reports the model the caller requested, since the OpenAI response requires model
  • safety_identifier is accepted on the request model but not forwarded to System One providers, which have no field for it; whether it drops under drop_params and 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 caller
  • osv-scan is red at this tip because next 16.3.6 in ui/litellm-dashboard/package-lock.json matches 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
  • Pre-existing on both legs: with 2 uvicorn workers and no Redis, a key removed through /key/delete keeps 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 check

Final 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

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 (request input/questions, response answers/usage with required token detail fields, plus hidden_params off the wire). Adds systemone.py to translate between that shape and the Jev/System One wire format (state, map-keyed questions/answers, noul/criteria) for TypeSafe-style providers.

Translation covers predicate→noul, choice/score→criteria, flattening message input to state, positional keys for unnamed questions, and building ordered OpenAI answers with filled total_tokens and 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/decisions and litellm/types/decisions.py behavior 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.

@devin-ai-integration

devin-ai-integration Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@CLAassistant

CLAassistant commented Oct 7, 2026 •

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.


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.

@greptile-apps

greptile-apps Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Medium risk] Adds OpenAI Decisions API types and System One translation layer.

This PR appears safe to merge; no new blocking issue was found

What we checked:

  • Images are not silently dropped: _part_text rejects image parts before the helper returns a provider request

Summary

Adds OpenAI Decisions models and helpers that translate requests and responses to and from Jev System One. The endpoint still uses the existing types, so its behavior does not change in this PR.

  • The translator preserves question order and fills required response usage fields
  • Unsupported images, boolean choices, repeated choice values, and duplicate question names are rejected
  • No new actionable issues were found. mateo-berri accepted the existing exception approach because it follows the shared provider error contract

Reviews (3) · Last reviewed commit: "fix(decisions): import assert_never from..." · Reviewed by Greptile

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

@codspeed

codspeed Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_decisions_openai_spec (7a175a5) with main (df23f11)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (225fd3b) during the generation of this report, so df23f11 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩

@codecov

codecov Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.98990% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/llms/base_llm/decisions/systemone.py 97.97% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

kerry and others added 3 commits October 7, 2026 21:10
…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>
@devin-ai-integration
devin-ai-integration Bot force-pushed the litellm_decisions_openai_spec branch from a461ae4 to 7e20e92 Compare October 7, 2026 21:10
kerry and others added 4 commits October 7, 2026 21:26
…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>
@mateo-berri

Copy link
Copy Markdown
Contributor

Reviewing and landing this stack bottom up now. Please hold further pushes or rebases on the four branches until it is on main

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

Comment thread litellm/llms/base_llm/decisions/systemone.py
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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.

@mateo-berri mateo-berri left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@mateo-berri
mateo-berri merged commit 79f62db into main Oct 8, 2026
103 of 105 checks passed
@mateo-berri
mateo-berri deleted the litellm_decisions_openai_spec branch October 8, 2026 00:42
mateo-berri added a commit that referenced this pull request Oct 8, 2026
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
DeoLeung pushed a commit to aiyou178/litellm that referenced this pull request Oct 11, 2026
…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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants