Skip to content

feat(decisions): backport /v1/systemone, OpenAI-format /v1/decisions and the OpenAI Decisions provider to stable/1.104.x for v1.104.2 (#44236, #45184, #45214) - #45190

Merged
mateo-berri merged 17 commits into
stable/1.104.xfrom
litellm_backport_systemone_stable_1_104_x
Oct 8, 2026
Merged

mateo-berri merged 17 commits into
stable/1.104.xfrom
litellm_backport_systemone_stable_1_104_x

Conversation

@mateo-berri

@mateo-berri mateo-berri commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

TLDR

Backport of #44236, #45184 and #45214 to stable/1.104.x, one commit per source commit with the original author kept. #44236 is merged on main and comes from its squash 8b1990b. #45184 and #45214 are still open, so they come from their branch commits: 3339469, 29e7154, 7ef1329 and b6bb885 for #45184, then b94bbbc, d209a85, 0dd207f and 1348de8 for #45214. This PR needs a refresh if either changes before it merges. On this line the #44236 pick needed hand work: the lazy route helpers in _lazy_features.py are ported by hand, managed agent authorization and the integration suites under tests/integration are not on the line so their hunks are dropped, and the lazy-disabled precedence test is dropped because attach_lazy_features takes no feature list here. The #45184 pick drops the playground System One tab files, which are not on the line, and the hidden_params property, which comes from #44668. The integration test follow-up 77872f6 is dropped since the decisions integration suites are not on the line, and the new 400 tests land without the lazy feature helper the line does not have. The #45214 picks keep this line's _get_httpx_client and _hidden_params names, with one small test commit so the new OpenAI provider test reads _hidden_params too. One line-native fix, 36894a1, gives the IR extra fields a default_factory, because Python 3.11 rejects the shared MappingProxyType default and import litellm failed there. 7df9c06 then picks #45214's 39012f1, which writes that default the same way #45214 does and bills cached and cache-write tokens once under custom pricing, since they were added on top of input_tokens that already include them. Version is bumped to 1.104.2 in its own commit, one patch above v1.104.1. c65d369 and 5d2cc10 pick e465919 and a2951b3, so the openai provider also reads litellm.api_key, litellm.openai_key, litellm.api_base and OPENAI_API_BASE like other OpenAI calls. f0cb1d5 picks e4081f7, so System One choice or score questions without instructions work on OpenAI deployments and single-option questions get a clear 400 before any upstream call. 7446e02 picks the guardrail fix from #45184's 855f0bb, as merged into #45214 at c19ada6, so OpenAI-format responses keep guardrail_information when the caller sets include_guardrail_response

Problem this solves:

  • stable/1.104.x has no Decisions endpoint at all
  • Callers want OpenAI's Decisions request and response shape at /v1/decisions
  • System One callers need their own shape on a separate route
  • OpenAI's own Decisions models, like gpt-6-luna, can't be called

How it solves it:

Picks:

User Flow

Before: a developer on stable/1.104.x who wants a yes/no, choice, or score decision from a decider model gets 404 on both routes

  1. They send POST https://litellm-domain/v1/systemone with a System One body (state, questions keyed by name)
  2. A 404 {"detail":"Not Found"} comes back
  3. They send POST https://litellm-domain/v1/decisions with an OpenAI Decisions body (input, questions list)
  4. A 404 {"detail":"Not Found"} comes back, with or without an input_image part

After: both routes answer with real decisions from the upstream model, each in its own shape

  1. They send POST https://litellm-domain/v1/systemone with the same System One body
  2. A 200 comes back with answers keyed by question name and usage input and output tokens
  3. They send POST https://litellm-domain/v1/decisions with the same OpenAI Decisions body
  4. A 200 comes back with an answers list and usage.total_tokens
  5. With an input_image part, OpenAI gpt-6-luna answers 200, while text-only Jev returns a clear 400

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

Proxy started from a checkout of each commit with python -m litellm.proxy.proxy_cli --port <port> --config decisions_config.yaml, port 4013 for Before and 4012 for After so both ran side by side, calling Perplexity, OpenAI and TypeSafe for real

model_list:
  - model_name: decider
    litellm_params:
      model: perplexity/pplx-decider-v1-27b
      api_key: os.environ/PERPLEXITY_API_KEY
  - model_name: luna
    litellm_params:
      model: openai/gpt-6-luna
      api_key: os.environ/OPENAI_API_KEY
  - model_name: jev
    litellm_params:
      model: typesafe/jev-latest
      api_key: os.environ/TYPESAFE_API_KEY

general_settings:
  master_key: os.environ/DECISIONS_PROOF_MASTER_KEY

systemone_body.json:

{
  "model": "decider",
  "state": "Customer wrote: I was charged twice for my order #4411 and want one of the charges refunded today.",
  "questions": {
    "is_refund_request": {"type": "noul", "instructions": "Is the customer asking for a refund?"},
    "urgency": {"type": "choice", "instructions": "How urgent is this?", "criteria": {"low": "can wait a week", "high": "needs action today"}},
    "frustration": {"type": "score", "instructions": "How frustrated is the customer?", "criteria": ["calm", "mildly annoyed", "angry"]}
  }
}

openai_body.json:

{
  "model": "decider",
  "input": [{"role": "user", "type": "message", "content": [{"type": "input_text", "text": "Customer wrote: I was charged twice for my order #4411 and want one of the charges refunded today."}]}],
  "questions": [
    {"type": "predicate", "name": "is_refund_request", "instructions": "Is the customer asking for a refund?"},
    {"type": "choice", "name": "needs_human", "instructions": "Should a human agent take over?", "choices": [{"value": true, "description": "billing dispute that needs a person"}, {"value": false}]},
    {"type": "score", "name": "frustration", "instructions": "How frustrated is the customer?", "levels": [{"label": "calm"}, {"label": "mildly annoyed"}, {"label": "angry", "description": "uses hostile language"}]}
  ]
}

luna_openai_body.json, jev_systemone_body.json and jev_openai_body.json are the bodies above with model set to luna or jev

luna_image_body.json, with jev_image_body.json the same except "model": "jev":

{"model": "luna", "input": [{"type": "message", "role": "user", "content": [{"type": "input_text", "text": "Here is the swatch the customer uploaded."}, {"type": "input_image", "image_url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAAlC+aJAAAAeUlEQVR4nO3PQQkAMAzAwEqof1WTNBF7HINABFzm7H7dcEEDWtCAFjSgBQ1oQQNa0IAWNKAFDWhBA1rQgBY0oAUNaEEDWtCAFjSgBQ1oQQNa0IAWNKAFDWhBA1rQgBY0oAUNaEEDWtCAFjSgBQ1oQQNa0IAWNKAFj11mTwDigCLoNgAAAABJRU5ErkJggg==", "detail": "low"}]}], "questions": [{"type": "predicate", "name": "is_red", "instructions": "Is the swatch mostly red?"}, {"type": "choice", "name": "color", "instructions": "Which color dominates the swatch?", "choices": [{"value": "red"}, {"value": "green"}, {"value": "blue"}]}], "safety_identifier": "proof-user-1"}

Before (48963be)

System One format at /v1/systemone, Perplexity decider

  1. curl -sS -i localhost:4013/v1/systemone -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d @systemone_body.json
  2. Output:
HTTP/1.1 404 Not Found
{"detail":"Not Found"}

OpenAI format at /v1/decisions, Perplexity decider

  1. curl -sS -i localhost:4013/v1/decisions -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d @openai_body.json
  2. Output:
HTTP/1.1 404 Not Found
{"detail":"Not Found"}

OpenAI format at /v1/decisions, OpenAI gpt-6-luna

  1. curl -sS -i localhost:4013/v1/decisions -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d @luna_openai_body.json
  2. Output:
HTTP/1.1 404 Not Found
{"detail":"Not Found"}

OpenAI format with an image at /v1/decisions, OpenAI gpt-6-luna

  1. curl -sS -i localhost:4013/v1/decisions -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d @luna_image_body.json
  2. Output:
HTTP/1.1 404 Not Found
{"detail":"Not Found"}

System One format at /v1/systemone, TypeSafe Jev

  1. curl -sS -i localhost:4013/v1/systemone -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d @jev_systemone_body.json
  2. Output:
HTTP/1.1 404 Not Found
{"detail":"Not Found"}

OpenAI format at /v1/decisions, TypeSafe Jev

  1. curl -sS -i localhost:4013/v1/decisions -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d @jev_openai_body.json
  2. Output:
HTTP/1.1 404 Not Found
{"detail":"Not Found"}

OpenAI format with an image at /v1/decisions, TypeSafe Jev

  1. curl -sS -i localhost:4013/v1/decisions -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d @jev_image_body.json
  2. Output:
HTTP/1.1 404 Not Found
{"detail":"Not Found"}

After (7df9c06)

System One format at /v1/systemone, Perplexity decider

  1. curl -sS -i localhost:4012/v1/systemone -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d @systemone_body.json
  2. Output:
HTTP/1.1 200 OK
x-litellm-response-cost: 1.336e-05
{"model":"pplx-decider-v1-27b","answers":{"is_refund_request":{"type":"noul","noul":0.9996314337402352},"urgency":{"type":"choice","choice":"high","confidence":0.9946260245218759,"probabilities":{"low":0.0026869877390619935,"high":0.9973130122609379}},"frustration":{"type":"score","score":0.7007878156583303,"confidence":0.44895551684927115,"legend":{"0":"calm","1":"mildly annoyed","2":"angry"},"probabilities":{"0":0.3332875865544111,"1":0.6326370112328474,"2":0.03407540221274142}}},"usage":{"input_tokens":334,"output_tokens":3}}

OpenAI format at /v1/decisions, Perplexity decider

  1. curl -sS -i localhost:4012/v1/decisions -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d @openai_body.json
  2. Output:
HTTP/1.1 200 OK
x-litellm-response-cost: 1.3520000000000001e-05
{"model":"pplx-decider-v1-27b","answers":[{"type":"predicate","name":"is_refund_request","probability":0.9996314337402352},{"type":"choice","name":"needs_human","choice":true,"probabilities":[{"value":true,"probability":0.967363826697176},{"value":false,"probability":0.0326361733028241}],"confidence":0.934727653394352},{"type":"score","name":"frustration","score":0.5398263218326869,"probabilities":[{"value":0,"label":"calm","probability":0.46310176468759334},{"value":1,"label":"mildly annoyed","probability":0.5339701487921265},{"value":2,"label":"angry","probability":0.0029280865202802255}],"confidence":0.3009552231881897}],"usage":{"input_tokens":338,"input_tokens_details":{"cached_tokens":0,"cache_write_tokens":0},"output_tokens":3,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":341}}

OpenAI format at /v1/decisions, OpenAI gpt-6-luna

  1. curl -sS -i localhost:4012/v1/decisions -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d @luna_openai_body.json
  2. Output:
HTTP/1.1 200 OK
x-litellm-response-cost: 4.01e-05
{"model":"gpt-6-luna","answers":[{"type":"predicate","name":"is_refund_request","probability":1.0},{"type":"choice","name":"needs_human","choice":true,"probabilities":[{"value":true,"probability":0.99},{"value":false,"probability":0.01}],"confidence":0.98},{"type":"score","name":"frustration","score":0.74,"probabilities":[{"value":0,"label":"calm","probability":0.26},{"value":1,"label":"mildly annoyed","probability":0.74},{"value":2,"label":"angry","probability":0.0}],"confidence":0.61}],"usage":{"input_tokens":401,"input_tokens_details":{"cached_tokens":0,"cache_write_tokens":0},"output_tokens":0,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":401}}

OpenAI format with an image at /v1/decisions, OpenAI gpt-6-luna

  1. curl -sS -i localhost:4012/v1/decisions -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d @luna_image_body.json
  2. Output:
HTTP/1.1 200 OK
x-litellm-response-cost: 2.77e-05
{"model":"gpt-6-luna","answers":[{"type":"predicate","name":"is_red","probability":1.0},{"type":"choice","name":"color","choice":"red","probabilities":[{"value":"red","probability":1.0},{"value":"green","probability":0.0},{"value":"blue","probability":0.0}],"confidence":1.0}],"usage":{"input_tokens":277,"input_tokens_details":{"cached_tokens":0,"cache_write_tokens":0},"output_tokens":0,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":277}}

System One format at /v1/systemone, TypeSafe Jev

  1. curl -sS -i localhost:4012/v1/systemone -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d @jev_systemone_body.json
  2. Output:
HTTP/1.1 200 OK
x-litellm-response-cost: 1.6422e-05
{"model":"jev-1.13.0","answers":{"is_refund_request":{"type":"noul","noul":0.99},"urgency":{"type":"choice","choice":"high","confidence":1.0,"probabilities":{"low":0.0,"high":1.0}},"frustration":{"type":"score","score":0.96,"confidence":0.88,"legend":{"0":"calm","1":"mildly annoyed","2":"angry"},"probabilities":{"0":0.06,"1":0.92,"2":0.02}}},"usage":{"input_tokens":391,"output_tokens":67}}

OpenAI format at /v1/decisions, TypeSafe Jev

  1. curl -sS -i localhost:4012/v1/decisions -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d @jev_openai_body.json
  2. Output:
HTTP/1.1 200 OK
x-litellm-response-cost: 1.6422e-05
{"model":"jev-1.13.0","answers":[{"type":"predicate","name":"is_refund_request","probability":0.99},{"type":"choice","name":"needs_human","choice":true,"probabilities":[{"value":true,"probability":0.59},{"value":false,"probability":0.41}],"confidence":0.18},{"type":"score","name":"frustration","score":0.83,"probabilities":[{"value":0,"label":"calm","probability":0.17},{"value":1,"label":"mildly annoyed","probability":0.83},{"value":2,"label":"angry","probability":0.0}],"confidence":0.74}],"usage":{"input_tokens":391,"input_tokens_details":{"cached_tokens":0,"cache_write_tokens":0},"output_tokens":68,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":459}}

OpenAI format with an image at /v1/decisions, TypeSafe Jev

  1. curl -sS -i localhost:4012/v1/decisions -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d @jev_image_body.json
  2. Output:
HTTP/1.1 400 Bad Request
x-litellm-response-cost: 0
{"error":{"message":"litellm.BadRequestError: Decisions provider 'typesafe' cannot serve this request: input_image content parts are not supported because System One providers accept text input only\n\nLiteLLM: model group 'jev' failed with the error above. No fallback was attempted.","type":"invalid_request_error","param":null,"code":"400"}}

Custom pricing (7df9c06)

A second proxy with per-token prices on the deployment, called twice with a ~1.8k-token input to see whether OpenAI reports cached tokens

model_list:
  - model_name: luna-priced
    litellm_params:
      model: openai/gpt-6-luna
      api_key: os.environ/OPENAI_API_KEY
      input_cost_per_token: 0.000001
      output_cost_per_token: 0.000002
      cache_read_input_token_cost: 0.0000001
      cache_creation_input_token_cost: 0.00000125

general_settings:
  master_key: os.environ/DECISIONS_PROOF_MASTER_KEY
=== call 1: curl -i localhost:4015/v1/decisions -d @cost_openai_body.json
HTTP/1.1 200 OK
x-litellm-response-cost: 0.001854
{"model":"gpt-6-luna","answers":[{"type":"predicate","name":"is_refund_request","probability":1.0}],"usage":{"input_tokens":1854,"input_tokens_details":{"cached_tokens":0,"cache_write_tokens":0},"output_tokens":0,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":1854}}
expected (input-cached-write)*1e-6 + cached*1e-7 + write*1.25e-6 + output*2e-6 = 0.001854000; proxy billed 0.001854000; match=True

=== call 2: curl -i localhost:4015/v1/decisions -d @cost_openai_body.json
HTTP/1.1 200 OK
x-litellm-response-cost: 0.001854
{"model":"gpt-6-luna","answers":[{"type":"predicate","name":"is_refund_request","probability":1.0}],"usage":{"input_tokens":1854,"input_tokens_details":{"cached_tokens":0,"cache_write_tokens":0},"output_tokens":0,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":1854}}
expected (input-cached-write)*1e-6 + cached*1e-7 + write*1.25e-6 + output*2e-6 = 0.001854000; proxy billed 0.001854000; match=True

Billing matches the configured rates. OpenAI returned 0 cached tokens on both calls

OpenAI key from litellm_settings (c65d369)

The proxy runs with no OPENAI_API_KEY in its environment and the key set only through litellm_settings.openai_key

model_list:
  - model_name: luna
    litellm_params:
      model: openai/gpt-6-luna

litellm_settings:
  openai_key: os.environ/DECISIONS_PROOF_OPENAI_KEY

general_settings:
  master_key: os.environ/DECISIONS_PROOF_MASTER_KEY

Before the pick, the provider ignored litellm.openai_key and returned this (captured at #45214's 39012f1, which has the same provider code as 7df9c06)

$ curl -sS -i localhost:4000/v1/decisions -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d @body.json
HTTP/1.1 401 Unauthorized
x-litellm-response-cost: 0
{"error":{"message":"litellm.AuthenticationError: Missing API key for Decisions provider 'openai'\n\nLiteLLM: model group 'luna' failed with the error above. No fallback was attempted.","type":"authentication_error","param":null,"code":"401"}}

After, at c65d369

$ curl -sS -i localhost:4000/v1/decisions -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d @body.json
HTTP/1.1 200 OK
x-litellm-response-cost: 1.68e-05
{"model":"gpt-6-luna","answers":[{"type":"predicate","name":"is_defect","probability":0.52}],"usage":{"input_tokens":168,"input_tokens_details":{"cached_tokens":0,"cache_write_tokens":0},"output_tokens":0,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":168}}

System One questions without instructions (f0cb1d5)

Same luna config as above, with the key from litellm_settings.openai_key. Before, at 5d2cc10, OpenAI rejected criteria-only choice and score questions, and the single-option question cost an upstream round trip

$ cat noinst.json
{"model": "luna", "state": "My new phone arrived with a cracked screen and I want my money back.", "questions": {"action": {"type": "choice", "criteria": {"refund": "Customer wants money back", "replace": "Customer wants a new unit", "escalate": null}}, "severity": {"type": "score", "criteria": ["cosmetic", "partly usable", "unusable"]}}}
$ curl -sS -i localhost:4000/v1/systemone -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d @noinst.json
HTTP/1.1 400 Bad Request
x-litellm-response-cost: 0
{"error":{"message":"litellm.BadRequestError: OpenAIException - {\"error\":{\"message\":\"Missing required parameter: 'questions[0].instructions'.\",\"type\":\"invalid_request_error\",\"param\":\"questions[0].instructions\",\"code\":\"missing_required_parameter\"}}\n\nLiteLLM: model group 'luna' failed with the error above. No fallback was attempted.","type":"invalid_request_error","param":null,"code":"400"}}

$ cat oneopt.json
{"model": "luna", "state": "My new phone arrived with a cracked screen.", "questions": {"action": {"type": "choice", "instructions": "What should we do?", "criteria": {"refund": "Customer wants money back"}}}}
$ curl -sS -i localhost:4000/v1/systemone -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d @oneopt.json
HTTP/1.1 400 Bad Request
x-litellm-response-cost: 0
{"error":{"message":"litellm.BadRequestError: OpenAIException - {\"error\":{\"message\":\"Invalid 'questions[0].choices': array too short. Expected an array with minimum length 2, but got an array with length 1 instead.\",\"type\":\"invalid_request_error\",\"param\":\"questions[0].choices\",\"code\":\"array_below_min_length\"}}\n\nLiteLLM: model group 'luna' failed with the error above. No fallback was attempted.","type":"invalid_request_error","param":null,"code":"400"}}

After, at f0cb1d5, which picks #45214's e4081f7, the criteria-only questions get real answers from OpenAI and the single-option question gets a litellm 400 that says why

$ cat noinst.json
{"model": "luna", "state": "My new phone arrived with a cracked screen and I want my money back.", "questions": {"action": {"type": "choice", "criteria": {"refund": "Customer wants money back", "replace": "Customer wants a new unit", "escalate": null}}, "severity": {"type": "score", "criteria": ["cosmetic", "partly usable", "unusable"]}}}
$ curl -sS -i localhost:4000/v1/systemone -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d @noinst.json
HTTP/1.1 200 OK
x-litellm-response-cost: 2.48e-05
{"model":"gpt-6-luna","answers":{"action":{"type":"choice","choice":"refund","confidence":1.0,"probabilities":{"refund":1.0,"replace":0.0,"escalate":0.0}},"severity":{"type":"score","score":1.17,"confidence":0.0,"legend":{"0":"cosmetic","1":"partly usable","2":"unusable"},"probabilities":{"0":0.29,"1":0.25,"2":0.46}}},"usage":{"input_tokens":248,"output_tokens":0}}

$ cat oneopt.json
{"model": "luna", "state": "My new phone arrived with a cracked screen.", "questions": {"action": {"type": "choice", "instructions": "What should we do?", "criteria": {"refund": "Customer wants money back"}}}}
$ curl -sS -i localhost:4000/v1/systemone -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d @oneopt.json
HTTP/1.1 400 Bad Request
x-litellm-response-cost: 0
{"error":{"message":"litellm.BadRequestError: Decisions provider 'openai' cannot serve this request: OpenAI needs at least 2 choices or levels on every choice or score question\n\nLiteLLM: model group 'luna' failed with the error above. No fallback was attempted.","type":"invalid_request_error","param":null,"code":"400"}}

guardrail_information on OpenAI-format responses

Same both_config.yaml, with include_guardrail_response: true added to luna_openai_body.json and jev_openai_body.json as luna_guardrail_body.json and jev_guardrail_body.json

for m in luna jev; do echo "=== $m"; curl -sS -i localhost:$PORT/v1/decisions -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d @${m}_guardrail_body.json | grep -iE '^HTTP|x-litellm-response-cost:|^\{'; done

Before, at f0cb1d5, both responses drop guardrail_information even though the caller asked for it

=== luna
HTTP/1.1 200 OK
x-litellm-response-cost: 4.01e-05
{"model":"gpt-6-luna","answers":[{"type":"predicate","name":"is_refund_request","probability":1.0},{"type":"choice","name":"needs_human","choice":true,"probabilities":[{"value":true,"probability":0.99},{"value":false,"probability":0.01}],"confidence":0.98},{"type":"score","name":"frustration","score":0.74,"probabilities":[{"value":0,"label":"calm","probability":0.26},{"value":1,"label":"mildly annoyed","probability":0.74},{"value":2,"label":"angry","probability":0.0}],"confidence":0.61}],"usage":{"input_tokens":401,"input_tokens_details":{"cached_tokens":0,"cache_write_tokens":0},"output_tokens":0,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":401}}

=== jev
HTTP/1.1 200 OK
x-litellm-response-cost: 1.6422e-05
{"model":"jev-1.13.0","answers":[{"type":"predicate","name":"is_refund_request","probability":0.99},{"type":"choice","name":"needs_human","choice":true,"probabilities":[{"value":true,"probability":0.64},{"value":false,"probability":0.36}],"confidence":0.28},{"type":"score","name":"frustration","score":0.85,"probabilities":[{"value":0,"label":"calm","probability":0.15},{"value":1,"label":"mildly annoyed","probability":0.85},{"value":2,"label":"angry","probability":0.0}],"confidence":0.78}],"usage":{"input_tokens":391,"input_tokens_details":{"cached_tokens":0,"cache_write_tokens":0},"output_tokens":68,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":459}}

After, at 7446e02, both responses carry guardrail_information (empty here since no guardrails are configured)

=== luna
HTTP/1.1 200 OK
x-litellm-response-cost: 4.01e-05
{"model":"gpt-6-luna","answers":[{"type":"predicate","name":"is_refund_request","probability":1.0},{"type":"choice","name":"needs_human","choice":true,"probabilities":[{"value":true,"probability":0.99},{"value":false,"probability":0.01}],"confidence":0.98},{"type":"score","name":"frustration","score":0.74,"probabilities":[{"value":0,"label":"calm","probability":0.26},{"value":1,"label":"mildly annoyed","probability":0.74},{"value":2,"label":"angry","probability":0.0}],"confidence":0.61}],"usage":{"input_tokens":401,"input_tokens_details":{"cached_tokens":0,"cache_write_tokens":0},"output_tokens":0,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":401},"guardrail_information":[]}

=== jev
HTTP/1.1 200 OK
x-litellm-response-cost: 1.6422e-05
{"model":"jev-1.13.0","answers":[{"type":"predicate","name":"is_refund_request","probability":0.99},{"type":"choice","name":"needs_human","choice":true,"probabilities":[{"value":true,"probability":0.68},{"value":false,"probability":0.32}],"confidence":0.36},{"type":"score","name":"frustration","score":0.81,"probabilities":[{"value":0,"label":"calm","probability":0.19},{"value":1,"label":"mildly annoyed","probability":0.81},{"value":2,"label":"angry","probability":0.0}],"confidence":0.71}],"usage":{"input_tokens":391,"input_tokens_details":{"cached_tokens":0,"cache_write_tokens":0},"output_tokens":68,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":459},"guardrail_information":[]}

Type

New Feature

Caveats (if any)

Medium

Low

  • OpenAI and TypeSafe returned 0 cached tokens on every repeat call, so the cached-token billing fix is proven by test_custom_token_pricing_bills_cached_decisions_input_tokens_once, which fails without it
  • input_image parts still return 400 on text-only System One providers
  • safety_identifier reaches OpenAI but System One providers have no such field
  • The playground System One tab is not on this line
  • Feature backport, not a P0, on a direct ask to ship it in the latest rc and stable
  • System One questions with a single choice or level get a 400 on OpenAI deployments, since OpenAI needs at least 2
  • _prepare_request raising directly and decisions TPM accounting both mirror feat(decisions): add unified /v1/decisions endpoint for Jev-compatible providers #44236 as merged on main
  • Greptile's note that the route ownership tests check structure is left as is, since they mirror feat(decisions): add unified /v1/decisions endpoint for Jev-compatible providers #44236 and every route also has functional tests

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

Note

Medium Risk
Introduces new authenticated data-plane endpoints and multi-provider request translation with billing and route-precedence changes; mistakes could mis-route traffic or mis-price tokens, but scope is additive with heavy test coverage.

Overview
Adds a Decisions / System One capability to stable 1.104.2: structured yes/no, choice, and score questions over state or OpenAI-style input, routed through a shared internal representation to upstream decider APIs.

SDK and routing: New litellm.decisions (decisions / adecisions) with provider configs for Perplexity, TypeSafe, OpenRouter, Cloudflare Workers AI, Strands Decider, and OpenAI. The LiteLLM Router exposes the same call types; gateway and proxy allowlists include /v1/decisions, /decisions, /v1/systemone, and /systemone.

Proxy: Lazy-loaded decisions_endpoints serve System One bodies on /systemone and OpenAI Decisions bodies on /decisions, wired through ProxyBaseLLMRequestProcessing as adecisions. Pass-through route registration now treats lazy stub routes as non-blocking so a config pass-through at /v1/decisions can win precedence when the feature has not loaded.

Cross-cutting: Pydantic types in litellm.types.decisions, IR transforms in llms/base_llm/decisions and per-provider modules, usage→litellm.Usage for billing/logging, evaluation health checks via adecisions, model pricing entries for decider models, and broad unit/proxy tests (CI shards updated). README/provider metadata list new providers; version bumped to 1.104.2.

Reviewed by Cursor Bugbot for commit 7446e02. Bugbot is set up for automated code reviews on this repo. Configure here.

devin-ai-integration Bot and others added 2 commits October 7, 2026 16:00
…e providers (#44236)

Backport of #44236 to stable/1.104.x.
Cherry-picked from 8b1990b (main). Adapted to this line: the lazy feature
helpers are ported by hand, managed agent authorization and the integration suites are not on
this line, and the lazy-disabled precedence test is dropped since attach_lazy_features takes no
feature list here.
…format at /v1/decisions

Backport of #45184 to stable/1.104.x.
Cherry-picked from 3339469 (litellm_systemone_endpoint, #45184 is not merged yet). The playground
System One tab and managed agent authorization are not on this line, so those hunks are dropped,
and the lazy-disabled precedence test is dropped as in the #44236 pick.
@CLAassistant

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 all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ mateo-berri
❌ devin-ai-integration[bot]
You have signed the CLA already but the status is still pending? Let us recheck it.

… 3.10

Backport of the #45184 follow-up to stable/1.104.x.
Cherry-picked from 29e7154 (litellm_systemone_endpoint).
@mateo-berri
mateo-berri force-pushed the litellm_backport_systemone_stable_1_104_x branch from d675b6d to b765179 Compare October 7, 2026 23:12
@greptile-apps

greptile-apps Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[High risk] Adds decisions API endpoints and provider integrations across the proxy.

The latest change appears safe to merge; no new actionable issue remains

What we checked:

  • Requested guardrail details disappear: OpenAIDecisionResponse now allows extra fields, which lets the shared helper copy guardrail_information onto the response

Summary

Backports Decisions support to stable version 1.104.2, with separate System One and OpenAI request formats, shared provider translation, routing, and billing

  • The latest commit lets OpenAI-format responses keep proxy-added guardrail_information
  • Adds a regression test for the requested field on both routes
  • Earlier fixes remain present: compatible imports, matching question limits, safe defaults, OpenAI settings, question translation, and test cleanup
  • mateo-berri accepted direct errors from _prepare_request and Decisions token accounting because this backport mirrors merged feat(decisions): add unified /v1/decisions endpoint for Jev-compatible providers #44236. mateo-berri also left the route ownership checks as-is because the routes have functional tests

Reviews (10) · Last reviewed commit: "fix(decisions): return guardrail_informa..." · Reviewed by Greptile

Comment thread litellm/decisions/openai_transformation.py Outdated
Comment thread litellm/types/decisions.py Outdated
Backport of the #45184 follow-up to stable/1.104.x.
Cherry-picked from 7ef1329 (litellm_systemone_endpoint).
Backport of the #45184 follow-up to stable/1.104.x.
Cherry-picked from b6bb885 (litellm_systemone_endpoint).
request=request,
fastapi_response=fastapi_response,
user_api_key_dict=user_api_key_dict,
route_type="adecisions",

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.

High: Decisions requests bypass TPM limits

An authenticated caller can repeatedly send large state/questions payloads through either new API without consuming their key/team TPM allowance: parallel_request_limiter_v3 does not estimate those input fields, and its success handler ignores DecisionsResponse, recording zero tokens and refunding any TPM reservation. Add decisions-specific input estimation and response-usage handling to the limiter before enabling this route.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Decisions TPM accounting matches #44236 as merged on main. This backport mirrors it, and limiter support for decisions belongs in a main follow-up

@veria-ai

veria-ai Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

PR overview

This PR backports the /v1/systemone endpoint and the OpenAI-format /v1/decisions endpoint to stable/1.104.x for v1.104.2.

One security issue remains open, with none addressed so far. Authenticated callers can repeatedly submit large decisions payloads without consuming their key or team token-per-minute allowance because the limiter does not account for decisions inputs or response usage. This permits token-limit bypass and potentially excessive resource consumption through either new endpoint.

Open issues (1)

Fixed/addressed: 0 · PR risk: 6/10

@mateo-berri
mateo-berri force-pushed the litellm_backport_systemone_stable_1_104_x branch from b765179 to ef6cbbc Compare October 7, 2026 23:28
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai please re-review the new tip: non-JSON bodies now return 400 and OpenAI-format questions are capped at the System One limit

Comment thread litellm/decisions/main.py
System One requests to openai/ models are translated to OpenAI's Decisions shape on the way out and OpenAI's answers are translated back, so both /v1/systemone and /v1/decisions can route to gpt-6-luna.

(cherry picked from commit b94bbbc)
…s IR

System One and OpenAI-format bodies now convert to one internal representation, and each provider translates it to its own wire format. OpenAI deployments receive the caller's messages, images, names, typed choice values, level descriptions and safety_identifier unchanged. System One providers return a 400 for image input. Cached and cache-write tokens are priced on both response shapes, and provider response extras survive the round trip.

(cherry picked from commit 0dd207f)
…table/1.104.x

The public hidden_params accessor comes from #44668, which is not on stable/1.104.x, so the backported test reads the private attribute like the rest of the file
@mateo-berri mateo-berri changed the title feat(decisions): backport /v1/systemone and OpenAI-format /v1/decisions to stable/1.104.x for v1.104.2 (#44236, #45184) feat(decisions): backport /v1/systemone, OpenAI-format /v1/decisions and the OpenAI Decisions provider to stable/1.104.x for v1.104.2 (#44236, #45184, #45214) Oct 8, 2026
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai review this again: the branch now also carries the #45214 OpenAI Decisions provider and shared IR picks

Comment thread litellm/types/decisions.py Outdated
Comment thread litellm/llms/openai/decisions/transformation.py Outdated
Comment thread litellm/llms/openai/decisions/transformation.py Outdated
Backport of #45214 to stable/1.104.x.
Cherry-picked from 1348de8 (litellm_openai_decisions_provider).

(cherry picked from commit 1348de8)

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

…nce under custom pricing

Backport of 39012f1 from #45214 to stable/1.104.x. The IR extra default now mirrors #45214 (shared _NO_EXTRA via default_factory).
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

…enai provider

Backport of #45214 to stable/1.104.x.
Cherry-picked from e465919 (litellm_openai_decisions_provider).
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

Comment thread litellm/llms/openai/decisions/transformation.py
…deployment test

Backport of #45214 to stable/1.104.x.
Cherry-picked from a2951b3 (litellm_openai_decisions_provider).
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor Author

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.

Stale Bugbot comment from a previous run.

…hout them and reject single-option questions
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor Author

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.

Stale Bugbot comment from a previous run.

…ns when requested

Backport of the #45184 and #45214 guardrail fix.
Cherry-picked from 855f0bb (litellm_systemone_endpoint) as merged into #45214 at c19ada6 (litellm_openai_decisions_provider).
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor Author

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 7446e02. Configure here.

@mateo-berri
mateo-berri merged commit fc3920b into stable/1.104.x Oct 8, 2026
10 of 11 checks passed
@mateo-berri
mateo-berri deleted the litellm_backport_systemone_stable_1_104_x branch October 8, 2026 05:28
doonga pushed a commit to greyrock-labs/home-ops that referenced this pull request Oct 8, 2026
…04.2) (#374)

This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [ghcr.io/berriai/litellm](https://images.chainguard.dev/directory/image/wolfi-base/overview) ([source](https://github.com/BerriAI/litellm)) | patch | `v1.104.0` → `v1.104.2` |

---

### Release Notes

<details>
<summary>BerriAI/litellm (ghcr.io/berriai/litellm)</summary>

### [`v1.104.2`](https://github.com/BerriAI/litellm/releases/tag/v1.104.2)

[Compare Source](BerriAI/litellm@v1.104.1...v1.104.2)

#### Verify Docker Image Signature

All LiteLLM Docker images are signed with [cosign](https://docs.sigstore.dev/cosign/overview/). Every release is signed with the same key introduced in [commit `0112e53`](BerriAI/litellm@0112e53).

**Verify using the pinned commit hash (recommended):**

A commit hash is cryptographically immutable, so this is the strongest way to ensure you are using the original signing key:

```bash
cosign verify \
  --key https://raw.githubusercontent.com/BerriAI/litellm/0112e53046018d726492c814b3644b7d376029d0/cosign.pub \
  ghcr.io/berriai/litellm:v1.104.2
```

**Verify using the release tag (convenience):**

Tags are protected in this repository and resolve to the same key. This option is easier to read but relies on tag protection rules:

```bash
cosign verify \
  --key https://raw.githubusercontent.com/BerriAI/litellm/v1.104.2/cosign.pub \
  ghcr.io/berriai/litellm:v1.104.2
```

Expected output:

```
The following checks were performed on each of these signatures:
  - The cosign claims were validated
  - The signatures were verified against the specified public key
```

***

#### What's Changed

- feat(decisions): backport /v1/systemone, OpenAI-format /v1/decisions and the OpenAI Decisions provider to stable/1.104.x for v1.104.2 ([#&#8203;44236](BerriAI/litellm#44236), [#&#8203;45184](BerriAI/litellm#45184), [#&#8203;45214](BerriAI/litellm#45214)) by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;45190](BerriAI/litellm#45190)

**Full Changelog**: <BerriAI/litellm@v1.104.1...v1.104.2>

### [`v1.104.1`](https://github.com/BerriAI/litellm/releases/tag/v1.104.1)

[Compare Source](BerriAI/litellm@v1.104.0...v1.104.1)

#### Verify Docker Image Signature

All LiteLLM Docker images are signed with [cosign](https://docs.sigstore.dev/cosign/overview/). Every release is signed with the same key introduced in [commit `0112e53`](BerriAI/litellm@0112e53).

**Verify using the pinned commit hash (recommended):**

A commit hash is cryptographically immutable, so this is the strongest way to ensure you are using the original signing key:

```bash
cosign verify \
  --key https://raw.githubusercontent.com/BerriAI/litellm/0112e53046018d726492c814b3644b7d376029d0/cosign.pub \
  ghcr.io/berriai/litellm:v1.104.1
```

**Verify using the release tag (convenience):**

Tags are protected in this repository and resolve to the same key. This option is easier to read but relies on tag protection rules:

```bash
cosign verify \
  --key https://raw.githubusercontent.com/BerriAI/litellm/v1.104.1/cosign.pub \
  ghcr.io/berriai/litellm:v1.104.1
```

Expected output:

```
The following checks were performed on each of these signatures:
  - The cosign claims were validated
  - The signatures were verified against the specified public key
```

***

#### What's Changed

- chore(release): refresh dependencies on stable/1.104.x and cut 1.104.1 by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;44777](BerriAI/litellm#44777)
- chore(docker): bump pgbouncer to 1.26.0 by [@&#8203;devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#&#8203;45012](BerriAI/litellm#45012)

**Full Changelog**: <BerriAI/litellm@v1.104.0...v1.104.1>

</details>

---

### Configuration

📅 **Schedule**: (in timezone America/New_York)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate CLI](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMzMuMCIsInVwZGF0ZWRJblZlciI6IjQ0LjEzMy4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJyZW5vdmF0ZS9jb250YWluZXIiLCJ0eXBlL3BhdGNoIl19-->

Reviewed-on: https://git.greyrock.io/todd/home-ops/pulls/374

This branch is waiting to be deployed

1 waiting deployment
e2e-changed — 7446e026 Waiting Oct 8, 2026 by mateo-berri via oauth #3766
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.

4 participants