Skip to content

feat(decisions): backport the rest of the decisions and System One launch to rc/1.106.0 - #45891

Merged
mateo-berri merged 18 commits into
rc/1.106.0from
litellm_backport_decisions_ui_rc_1_106_0
Oct 11, 2026
Merged

mateo-berri merged 18 commits into
rc/1.106.0from
litellm_backport_decisions_ui_rc_1_106_0

Conversation

@mateo-berri

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

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • rc/1.106.0 ships only part of the decisions launch
  • hosted_vllm, Databricks ai_decide and AgentCore Strands Decider are missing
  • The Playground tab still says System One and only calls one endpoint
  • The SDK has no litellm.systemone

How it solves it:

  • Cherry-picks the 15 merged decisions PRs the line lacks
  • One commit per PR, original author and squash sha kept
  • Regenerates schema.d.ts on this line in its own commit

Intentional product change: #44955 and #45846 are carried over as merged on main. /v1/decisions now refuses safety_identifier on providers that cannot take it unless drop_params is on, and litellm.decisions is trimmed to the OpenAI shape while the System One shape moves to litellm.systemone, with state= on litellm.decisions kept as a soft deprecation that warns and still routes, exactly as on main. #45200 also makes a registry miss on an auto-router row run the full add_deployment reconcile in registry_read_through.py, as merged on main. litellm.systemone

Included PRs

The line already had #45639, #45673, #45654, #45717 (through #45735), #45721 (through #45739) and #45663 (through #45888). With this PR the decisions backend, SDK and playground on rc/1.106.0 match main

Conflict notes. #45846 landed on main before #45663 but reaches this line after it, so litellm/decisions/call.py and its two importers are resolved to main's final state (supported_decisions_providers). schema.d.ts is generated, so it is regenerated with npm run gen:api in the last commit and never hand-merged. No version bump, since rc lines keep pyproject.toml at 1.106.0

Test isolation. cd141c6 picks main's per-test logging-worker flush hook into tests/unit/conftest.py so tests/unit/systemone/test_main.py passes with -n 4 --dist=loadscope, which LiteReview reproduced failing at acf7455. d5b0ad0 then drops the import line the pick duplicated. Both change test setup only

User Flow

Before

  1. An admin on rc/1.106.0 adds openrouter/typesafe/jev-1.13, POSTs /v1/decisions and gets 200 with no x-litellm-response-cost header, so the spend log stays at 0
  2. A developer POSTs /v1/decisions with model: databricks/... and gets 400 Unknown Decisions provider 'databricks'
  3. A developer passes safety_identifier to a TypeSafe model and it is silently dropped (200)
  4. In the Admin UI playground the tab reads "System One" with a single System One endpoint, a "Reset example" button, and the question type shows "yes / no" with no hover help

After

  1. The same POST /v1/decisions on OpenRouter returns x-litellm-response-cost: 1.4616e-05, the provider-reported cost, and /v1/responses bills the same way
  2. POST /v1/decisions on databricks/... returns 200 with answers from the Databricks serving endpoint
  3. safety_identifier to TypeSafe returns 400 UnsupportedParamsError unless the deployment sets drop_params: true, which returns 200
  4. The playground tab reads "Decisions" with both /v1/decisions and /v1/systemone selectable, an Example preset dropdown (Triage a bug report, Route a support ticket, Moderate a forum comment, Score a sales lead) plus a Clear button, a noul question type with hover help, and the models page banner links to the supported providers docs

Screenshots / Proof of Fix

Live /qa leg at acf7455 (cd141c6 on top changes test setup only) against the merge base (1bdfb67): two proxies from clean worktrees with --num_workers 2, their own Postgres and Redis, plus the dashboard dev server per leg, real TypeSafe, OpenRouter and Databricks keys from the shared vault, real spend

# B. POST /v1/decisions model=openrouter/typesafe/jev-1.13
BEFORE: HTTP 200, no x-litellm-response-cost header (cost 0.0)
AFTER:  HTTP 200, x-litellm-response-cost: 1.4616e-05, same answers
# C. POST /v1/responses model=openrouter gpt-5-nano
BEFORE: HTTP 200, x-litellm-response-cost-original: 0.0 while usage.cost = 1.1e-06
AFTER:  HTTP 200, x-litellm-response-cost: 1.1e-06

# D. POST /v1/decisions model=databricks/databricks-openjev-qwen35-4b
BEFORE: HTTP 400 Unknown Decisions provider 'databricks'
AFTER:  HTTP 200 {"model":"databricks-openjev-qwen35-4b","answers":[...]}

# E. POST /v1/decisions typesafe/jev-1.13.0 with safety_identifier
BEFORE: HTTP 200 (ignored)   AFTER: HTTP 400 UnsupportedParamsError; with drop_params true: HTTP 200

# F. GET /openapi.json: /v1/decisions and /v1/systemone gain summary, description and request example

# G. SDK: litellm.systemone exists after, litellm.decisions keeps the OpenAI shape

# Admin UI (Playwright, before :35017 / after :38031, logged in as admin)
H. /playground tabs   BEFORE: ["Chat","Compare","Compliance","System One Beta",...]  AFTER: [..., "Decisions Beta", ...]
I. models page banner  BEFORE: "... try them in the System One playground"  AFTER: "... Supported providers | Try decision models" linking docs/decisions#supported-providers
J. Add Model dropdown  BEFORE: "TypeSafe  Decision models: jev-1.13.0, jev-latest, jev-preview"  AFTER: "TypeSafe"
K. endpoint select     BEFORE: ["System One · /v1/systemone", "TypeSafe · /typesafe/v1/systemone"]  AFTER: ["/v1/decisions", "/v1/systemone"]
L. example controls    BEFORE: Reset example only  AFTER: Example preset dropdown with 4 presets and a Clear button
M. noul hover          BEFORE: "yes / no", no tooltip  AFTER: "noul" with "noul is a yes / no question. The model returns the probability that the answer is yes."

Two picks could not be exercised live and are blocked, not mocked: hosted_vllm (no vLLM server; the route now resolves the provider and fails on connect instead of 400 unknown provider) and the Strands Decider AgentCore call (no AgentCore runtime ARN credential in the shared vault)

Pre-Submission checklist

  • 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)

Type

New Feature

Caveats (if any)

Medium

Low

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
Includes a breaking change for safety_identifier on unsupported providers and touches proxy routing, billing headers, and AWS-signed outbound requests for AgentCore; scope is large but mostly additive behind new providers and routes.

Overview
This backports the rest of the decisions / System One launch: litellm.systemone / asystemone own the System One request shape (state + named questions), while litellm.decisions is narrowed to the OpenAI Decisions shape (input + ordered questions). Shared prepare/send logic moves to litellm/decisions/call.py, with a deprecation warning if state= is still passed to decisions.

Provider and transport work adds Databricks and hosted_vllm decision configs, expands Strands Decider for Bedrock AgentCore runtime ARNs (AWS signing, session headers, runtime error mapping), and threads litellm_params through the decisions HTTP path. safety_identifier is honored only when the provider supports it; otherwise callers get UnsupportedParamsError unless drop_params drops it. OpenRouter always advertises reasoning params, surfaces provider-reported cost on decisions/responses/pass-through, and bills from usage.cost when present.

Proxy/router routes /systemone via asystemone (distinct call type from /decisions), documents both endpoints in OpenAPI with inlined schemas, uses provider-specific health-check questions, blocks caller agentcore_runtime_session_id, and fully reconciles deployments when an auto-router model is missing from the in-memory registry. Complexity router can use Databricks as an OSS classifier backend.

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

devin-ai-integration Bot and others added 16 commits October 10, 2026 16:29
Backport of #45501 to rc/1.106.0.
Cherry-picked from 61e5f2d (main).
…e.cost and always allow reasoning params (#45640)

Backport of #45640 to rc/1.106.0.
Cherry-picked from 465f20f (main).
…lation cases (#45661)

Backport of #45661 to rc/1.106.0.
Cherry-picked from 097d018 (main).
… and auto-router decider (#45200)

Backport of #45200 to rc/1.106.0.
Cherry-picked from 883e321 (main).
…ke it unless drop_params drops it (#44955)

Backport of #44955 to rc/1.106.0.
Cherry-picked from 546402c (main).
… ARN (#45678)

Backport of #45678 to rc/1.106.0.
Cherry-picked from ed9c31c (main).
…the JSON editor VS Code light and dark themes (#45729)

Backport of #45729 to rc/1.106.0.
Cherry-picked from 512692d (main).
…docs and rename the System One playground to Decisions (#45732)

Backport of #45732 to rc/1.106.0.
Cherry-picked from 2d896d5 (main).
…r dropdown (#45755)

Backport of #45755 to rc/1.106.0.
Cherry-picked from 38d0333 (main).
Backport of #45817 to rc/1.106.0.
Cherry-picked from 33b9d84 (main).
… Clear button on the Decisions tab (#45848)

Backport of #45848 to rc/1.106.0.
Cherry-picked from dc77381 (main).
…n it on hover (#45841)

Backport of #45841 to rc/1.106.0.
Cherry-picked from 988cc7d (main).
…-reported cost (#45853)

Backport of #45853 to rc/1.106.0.
Cherry-picked from 5894968 (main).
…enAI shape (#45846)

Backport of #45846 to rc/1.106.0.
Cherry-picked from 25d9b05 (main).
The picks above each touched schema.d.ts on main against a different OpenAPI surface, so the file is regenerated on this line with npm run gen:api instead of hand-merged.
@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.

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

@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor Author

@litereview greptile code-review

@litereview

litereview Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

✅ greptile

Review incomplete.

Merge readiness: 3/5

Fix the new System One test's logging-queue isolation before merging. I reproduced its failure with the CI worker configuration; the same test passes alone. The focused backend run finished with 152 passed and 1 failed

Couldn’t complete

  • Could not consult the required Greptile knowledge base because this environment exposes no tool or resource for it
  • Greptile knowledge-base context: No Greptile knowledge-base tool was exposed. MCP resource and resource-template listings were empty
Checks and execution details
  • Failed · Focused backend tests (optional): Installed pinned dependencies with uv sync --frozen --group dev --no-install-project. Ran Decisions, System One, Databricks Decisions, Strands Decider, and OpenRouter Responses test files: 152 passed, 1 failed
  • Passed · System One failure isolation (optional): Confirmed the logging test passes alone and fails when running its full file, both serially and with -n 4 --dist=loadscope
  • Not run · Greptile knowledge-base context (required): No Greptile knowledge-base tool was exposed. MCP resource and resource-template listings were empty
  • Not run · Live provider and browser verification (optional): Did not run live provider calls or browser checks

View review · Reviewed acf745591166

@litereview

litereview Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

✅ code-review

Completed · Some checks not run.

Standards

I read the HEAD versions of the listed Python modules, the OpenRouter diff hunks, the endpoint and OpenAPI helper, and skimmed the System One UI. I checked them against root AGENTS.md (Coding conventions), ui/litellm-dashboard/AGENTS.md and the url-state skill it links to. I left out anything the LIT ruff rules, pyright or eslint already catch.

Hard violations (documented standard)

  • AGENTS.md: "Don't throw; model failures as values (one raise_public maps the error union…)". The new decisions core throws everywhere. In litellm/decisions/call.py there are 9 raise sites (L103, 171, 179, 184, 192, 199, 212, 225, 343). _UnsupportedSafetyIdentifier (L132) is turned into a value and then raised straight away at L212, so the new code only half-follows the pattern. The same goes for databricks/decisions/transformation.py L14/L50 and strands_decider/decisions/transformation.py L157.
  • AGENTS.md: "Fully typed; no … dict[str, Any] or just dict".
    • proxy/decisions_endpoints/endpoints.py L23/L28 validate into dict[str, object].
    • openrouter/responses/transformation.py builds TypeAdapter(dict[str, object]) inline on every call, at about L92.
    • The new endpoints systemone and decisions (L191, L234) have no return annotation.
  • AGENTS.md: "Composition over inheritance". StrandsDeciderDecisionsConfig, DatabricksDecisionsConfig, HostedVLLMDecisionsConfig and OpenRouterDecisionsConfig all subclass BaseDecisionsConfig and change its behaviour through class-attribute and method overrides plus super() calls (strands L113 and L161, hosted_vllm L25).
  • AGENTS.md: "No mutation". OpenRouterResponsesAPIConfig._set_provider_reported_cost (about L87) returns None and changes the response's hidden params in place. call.py L262 formatted.set_hidden_params(...) does the same.
  • AGENTS.md: comment rule. The long docstring on inline_request_body (custom_openapi_spec.py L461–467) is the kind of explanatory comment the rule forbids. The endpoint docstrings are OpenAPI text and get a pass.

Judgement calls (Fowler baseline)

  • Duplicated Code: asend and send (call.py L300–337) are the same apart from await. adecisions/decisions (main.py L75–128) and asystemone/systemone (systemone/main.py L51–104) repeat 11-argument bodies.
  • Data Clumps: model, api_key, api_base, timeout, custom_llm_provider, extra_headers, kwargs travel together through prepare_call, _prepare_decisions_call and _prepare_systemone_call. That bundle wants to be one type.
  • Refused Bequest / unused parameter: SystemOneRequestFields.validate(self, safety_identifier) (call.py L53) ignores its argument, and _ir_request L114 puts the value back later with replace.
  • Repeated Switches: the match on DecisionsRequestBody/OpenAIDecisionRequestBody appears twice (call.py L112 and L276).
  • Primitive Obsession: reject_other_format(field_name: str, other_function: str) (L340) and custom_llm_provider: str carried around when LlmProviders already exists. _provider_config L97 scans the enum by hand to find it.
  • Surprising behaviour: DatabricksDecisionsConfig.connection L48 only falls back to the env API key when api_base is None. That may be deliberate, but nothing in the code says so.

UI: I found no localStorage use. SystemOneUI.tsx L83–85 keeps the editor tab and endpoint in useState rather than nuqs, which is borderline under the url-state skill (it covers tabs).

Totals: 5 hard-violation groups and 6 judgement-call smells. The worst is that the new decisions core is built around raise instead of failure values.

Checks and execution details
  • Passed · Manual standards review against AGENTS.md files (required): Read the HEAD versions of the target Python modules and the OpenRouter diff hunks, and skimmed the System One UI, against root, tests and UI AGENTS.md.
  • Not run · Run lint/type tooling (optional): Not run. The brief said to skip anything the tooling enforces.

View review · Reviewed acf745591166


@pytest.mark.asyncio
async def test_router_asystemone_logs_the_systemone_call_type(
respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Medium · Drain pending logging callbacks before installing the recorder

Running this file with pytest tests/unit/systemone/test_main.py -n 4 --dist=loadscope fails at line 137. Earlier tests leave callbacks in GLOBAL_LOGGING_WORKER; after this test installs its recorder, flush() executes those callbacks against it, adding two unrelated call types. The test passes alone, so the assertion depends on test order. The new core-utils CI shard uses this worker configuration with no reruns. Request the existing drained_logging_worker fixture before installing the recorder, or drain callbacks in the preceding tests

File run: 6 passed, 1 failed; worker reported carrying 2 pending logging tasks onto the new loop

Isolated test: 1 passed

tests/unit/conftest.py:355 defines drained_logging_worker

Review by greptile · Reviewed acf745591166

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.

Fixed in cd141c6, picked the per-test logging-worker flush hook main carries in tests/unit/conftest.py, the file now passes with -n 4 --dist=loadscope

Comment thread litellm/decisions/call.py
)
provider_config: Final = _provider_config(upstream_model, provider)
if not upstream_model:
raise litellm.BadRequestError(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Medium · Decisions core throws instead of modelling failures as values

Root AGENTS.md says: "Don't throw; model failures as values." prepare_call has seven raise sites. _provider_config and reject_other_format each raise as well. _UnsupportedSafetyIdentifier is turned into a value and then raised straight away.

call.py L103, 171, 179, 184, 192, 199, 212, 225, 343

databricks/decisions/transformation.py L14, L50

strands_decider/decisions/transformation.py L157

Review by code-review · Reviewed acf745591166

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.

Faithful pick, litellm/decisions/call.py is byte-identical to main, so the failures-as-values refactor belongs on main first, then a follow-up pick

)

router: Final = APIRouter()
_REQUEST_DATA_ADAPTER: Final[TypeAdapter[dict[str, object]]] = TypeAdapter(dict[str, object])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Low · Coarse dict types and an untyped endpoint return

Root AGENTS.md says code must be fully typed, with no dict[str, Any] or bare dict. The request data and general settings are validated into dict[str, object], and the systemone/decisions handlers have no return annotation.

_REQUEST_DATA_ADAPTER: Final[TypeAdapter[dict[str, object]]]

async def systemone(...): # L191, no return type

Review by code-review · Reviewed acf745591166

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.

Faithful pick, endpoints.py matches main byte for byte. Typing changes land on main first and get picked afterwards, not on a release line

self._set_provider_reported_cost(terminal_response)
return response_event

@staticmethod

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Low · OpenRouter cost helper mutates the response in place

Root AGENTS.md says: "No mutation." _set_provider_reported_cost returns None and calls set_hidden_params on the response it was passed. It also builds TypeAdapter(dict[str, object]) on every call.

def _set_provider_reported_cost(response) -> None: ... set_hidden_params(response, {...})

Review by code-review · Reviewed acf745591166

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.

Faithful pick, this transformation.py is identical to main. A no-mutation rewrite lands on main first, this backport only carries merged code

}


class StrandsDeciderDecisionsConfig(BaseDecisionsConfig):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Low · Provider configs depend on inheritance rather than composition

Root AGENTS.md says: "Composition over inheritance." Each new decisions provider subclasses BaseDecisionsConfig and changes its behaviour through attribute and method overrides plus super() calls.

class StrandsDeciderDecisionsConfig(BaseDecisionsConfig)

return super().transform_decisions_response(...)

Review by code-review · Reviewed acf745591166

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.

Faithful pick, strands_decider transformation.py is identical to main. Changing the config class design is a main refactor, out of scope for a backport

Comment thread litellm/decisions/call.py
)


async def asend(call: DecisionsCall) -> DecisionsResponse | OpenAIDecisionResponse:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Low · Duplicated sync/async send and entry-point bodies (judgement call)

Possible Duplicated Code: asend and send differ only by await. The public sync/async wrappers in decisions/main.py and systemone/main.py repeat the same 11-argument forwarding.

asend L300-317 vs send L320-337

decisions/main.py L75-128; systemone/main.py L51-104

Review by code-review · Reviewed acf745591166

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.

Judgement call on code identical to main. Deduplicating send and asend is a main refactor, this backport ships merged code only

Comment thread litellm/decisions/call.py
state: DecisionsJSON | None
questions: SystemOneQuestions | None

def validate(self, safety_identifier: str | None) -> DecisionsRequestBody:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Low · SystemOneRequestFields.validate ignores its parameter (judgement call)

Possible Refused Bequest: this implementation of the shared validate(safety_identifier) signature drops the argument, and _ir_request puts it back later with replace.

def validate(self, safety_identifier: str | None) -> DecisionsRequestBody: return ...({"state": ..., "questions": ...})

Review by code-review · Reviewed acf745591166

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.

Same shape as main byte for byte. Any change to SystemOneRequestFields.validate lands on main first, then gets picked here

…allback checks stay isolated

Ports the pytest_runtest_call hook from tests/unit/conftest.py on main (#44146). Without it the asystemone call type test picks up adecisions callbacks queued by earlier tests in the same worker

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
(cherry picked from commit 23fec69)
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@litereview greptile code-review

@mateo-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@litereview

litereview Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

✅ greptile

Score: 4/5

The backport passed 582 targeted backend and dashboard tests. I found one low-severity SDK regression: malformed question inputs bypass LiteLLM’s BadRequestError handling

Checks and execution details
  • Passed · Repository and knowledge-base context (required): Read applicable AGENTS.md files and the attached writing skill. Queried the Greptile knowledge base for BerriAI/litellm
  • Passed · Decisions SDK and provider tests (optional): 153 tests passed across Decisions, System One, Databricks, Strands Decider, and OpenRouter Responses after installing locked Python dependencies
  • Passed · Proxy, routing, and billing tests (optional): 216 tests passed across Decisions endpoints, registry read-through, OpenAPI helpers, passthrough billing, JEV classification, and OpenRouter chat. Installed proxy-dev dependencies and generated Prisma before retrying
  • Passed · Dashboard Decisions tests (optional): 213 tests passed in 13 files covering the Decisions playground, request helpers, classifier configuration, and decision-model catalog. Installed package-lock dependencies using Node 24.16.0 after the system Node failed the engine requirement
  • Failed · Malformed SDK request compatibility (optional): The base raises BadRequestError (400) for questions=[5]; the head raises ValidationError without status_code in both synchronous and asynchronous SDK calls

View review · Reviewed cd141c6d3656

@litereview

litereview Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

✅ code-review

Completed · Some checks not run.

I've finished the Spec-axis review, including a read of the head source to check each finding. I found no missing PR and no regression against what the description promises. There are four findings: one partial requirement, two scope-creep items and one leftover from conflict resolution. I couldn't run tests: pytest isn't installed in this environment and I didn't set it up.

Spec

(a) Missing or partial

  • litellm.decisions still accepts the System One shape. Spec: "litellm.decisions only takes the OpenAI shape while the System One shape moves to litellm.systemone".
    • In litellm/decisions/main.py:34-46, a state= call still goes down the System One path with only a UserWarning, and the return types stay a union.
    • The other direction is strict: systemone rejects input outright.
    • So the split is a soft deprecation, not the hard trim the description states.

(b) Scope creep

  • registry_read_through.py: a registry miss on an auto-router row now reloads every deployment, which changes proxy-wide DB load. None of the 15 listed PR titles mentions it.
  • tests/unit/conftest.py: adds a global hook that flushes the logging worker. This is test infrastructure that isn't part of any listed decisions PR.

(c) Looks implemented but wrong

  • Conflict-resolution leftover: tests/unit/conftest.py:48 and :50 both import GLOBAL_LOGGING_WORKER, which looks like a hand-merge mistake from the cherry-picks.

Checked and matching the spec

  • safety_identifier: returns 400 unless drop_params is set; only the OpenAI provider config supports the field.
  • OpenRouter cost: decisions, Responses (including streaming) and pass-through all bill from the provider-reported cost. Reasoning params are always allowed.
  • New providers: Databricks and hosted_vllm are registered; Strands Decider AgentCore signing goes through sign_request.
  • Routing: /systemone now maps to asystemone.
  • OpenAPI: both endpoints have summaries, descriptions and examples.

Summary: 4 Spec findings. The worst is that litellm.decisions still accepts state=, which the spec says it should no longer do.

Checks and execution details
  • Passed · Spec vs. head source read-through (required): Checked decisions/call.py, decisions/main.py, systemone/main.py, the provider decisions transformations, the OpenRouter responses and chat code, and the proxy endpoints against /tmp/desc.md.
  • Not run · Unit tests (decisions / systemone / databricks / strands_decider) (optional): pytest is not installed in this environment (No module named pytest). I didn't set it up for this spec-only pass.

View review · Reviewed cd141c6d3656

Comment thread litellm/decisions/main.py
questions: OpenAIDecisionQuestions | SystemOneQuestions | None,
kwargs: Mapping[str, object],
) -> RequestFields:
if "state" not in kwargs:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Medium · litellm.decisions still accepts the System One state shape

The spec says "litellm.decisions only takes the OpenAI shape while the System One shape moves to litellm.systemone".

At head, _request_fields still routes state= calls to SystemOneRequestFields and only raises a UserWarning. The return types are still OpenAIDecisionResponse | DecisionsResponse.

The other direction is strict: systemone rejects input with reject_other_format. Either the PR text should say this is a soft deprecation, or decisions should reject state the same way.

litellm/decisions/main.py:34-46 deprecation-warn path

litellm/decisions/main.py:87,115 union return types

litellm/systemone/main.py:32 reject_other_format for input

Review by code-review · Reviewed cd141c6d3656

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.

Fair, main's #45846 is a soft deprecation too, main.py is byte-identical. Reworded the PR body to say state= warns rather than rejects

prisma_client=prisma_client, proxy_logging_obj=proxy_server.proxy_logging_obj
)
return proxy_server.llm_router is not None
if _holds_an_auto_router(rows):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Low · Auto-router registry miss now reloads every deployment; the spec doesn't mention it

_holds_an_auto_router triggers a full add_deployment reconcile, which changes proxy-wide DB load.

None of the 15 Included PR titles or the User Flow mentions this; only the Cursor summary does. If it comes with #45200, the PR description should say so.

registry_read_through.py: if _holds_an_auto_router(rows): await proxy_config.add_deployment(...)

Review by code-review · Reviewed cd141c6d3656

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.

It comes with #45200, which is in the Included PRs list, and the file matches main. Added a line to the body naming the auto-router reconcile

Comment thread tests/unit/conftest.py Outdated
from litellm._logging import ALL_LOGGERS # noqa: E402 # same import-time dependency
from litellm.litellm_core_utils.logging_worker import GLOBAL_LOGGING_WORKER # noqa: E402 # same import-time dependency
from litellm.anthropic_beta_headers_manager import reload_beta_headers_config # noqa: E402 # same import-time dependency
from litellm.litellm_core_utils.logging_worker import GLOBAL_LOGGING_WORKER # noqa: E402 # same import-time dependency

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Low · Duplicate GLOBAL_LOGGING_WORKER import plus an unrelated conftest hook

Line 50 repeats the import on line 48, which looks like a cherry-pick merge mistake.

The same file also adds a global pytest_runtest_call hook that flushes the logging worker. That isn't part of any listed decisions PR.

tests/unit/conftest.py:48 and :50 identical imports

tests/unit/conftest.py:163-187 new hook

Review by code-review · Reviewed cd141c6d3656

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.

Dropped the duplicated import in d5b0ad0. The flush hook is main's, picked so the systemone tests pass under xdist, now called out in the body

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

Comment thread tests/unit/conftest.py
_run_coroutine_if_needed(_complete_test_logging())
return
if worker_loop.is_running():
asyncio.run_coroutine_threadsafe(_complete_test_logging(), worker_loop).result()

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.

Test flush hook can deadlock

Medium Severity

When the logging worker loop is still running, _flush_completed_test_logging blocks on run_coroutine_threadsafe(...).result() with no timeout. If that loop belongs to the current thread, the thread waits for a flush/stop that cannot run, and the unit suite hangs after a test that left work in GLOBAL_LOGGING_WORKER.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit cd141c6. Configure here.

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.

Hook is byte-identical to main's. pytest_runtest_call yields after the test coroutine returns, so the current thread never owns a running loop here

Comment thread litellm/decisions/main.py
) -> RequestFields:
if "state" not in kwargs:
return OpenAIDecisionRequestFields(
input=decision_input, questions=_OPENAI_QUESTIONS_ADAPTER.validate_python(questions)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Low · Preserve BadRequestError for malformed SDK questions

Calling litellm.decisions(model="openai/test", input="test", questions=[5], api_key="test") now raises a Pydantic ValidationError without a status code. The base version raises litellm.BadRequestError with status 400. SDK callers that catch LiteLLM errors therefore miss this failure. _request_fields() runs before the validation exception handler in prepare_call(). Move these adapter validations, including the legacy state adapters, inside the same error-conversion boundary

Reproduced on both decisions and adecisions at the supplied head

Reconstructed the base decisions/main.py from the supplied diff and confirmed BadRequestError (400) for the same input

Review by greptile · Reviewed cd141c6d3656

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.

Faithful pick of #45846, litellm/decisions/main.py is byte-identical to main, so this ships there today and the fix lands on main first

…onftest pick

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@litereview greptile code-review

@mateo-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@litereview

litereview Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

✅ greptile

Completed · Some checks not run.

Score: 4/5

The backport looks close to merge-ready. I found one SDK error-handling regression: malformed questions now escape as Pydantic errors instead of BadRequestError

Verification: 391 tests passed across the selected files. Four OCR health-check cases failed because the local Rust extension was unavailable

Checks and execution details
  • Passed · Repository and knowledge-base context (required): Read applicable AGENTS.md files and the attached writing skill. Queried the Greptile knowledge base and read its failure-handling context
  • Passed · SDK and provider unit tests (optional): 153 passed across decisions, systemone, Databricks decisions, Strands Decider decisions, and OpenRouter Responses tests
  • Failed · Proxy, classifier, and health-check unit tests (optional): 238 passed; four OCR health-check cases failed with NoPythonImplementationError because the Rust extension was unavailable. Installed locked dependencies with uv and generated Prisma. Stopped the initial editable Rust build and used --no-install-project for the Python checks
  • Passed · SDK exception regression reproduction (optional): Confirmed the base raises BadRequestError (400) and the head raises ValidationError for the same malformed questions argument. Confirmed the async head path also exposes ValidationError
  • Not run · Live-provider and browser verification (optional): Did not exercise live provider services or the dashboard in a browser

View review · Reviewed d5b0ad087ce5

@litereview

litereview Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

✅ code-review

Review incomplete.

The review could not complete.

Couldn’t complete

  • The model endpoint rejected the request (HTTP 403). Check the selected model, credentials, and harness compatibility.

View review · Reviewed d5b0ad087ce5

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

Comment thread litellm/decisions/main.py
) -> RequestFields:
if "state" not in kwargs:
return OpenAIDecisionRequestFields(
input=decision_input, questions=_OPENAI_QUESTIONS_ADAPTER.validate_python(questions)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Low · Preserve BadRequestError for malformed SDK questions

Calling litellm.decisions(model="openai/test", input="hello", questions={"q": {"type": "predicate", "instructions": "yes?"}}, api_key="dummy") now raises a raw Pydantic ValidationError. The base returned litellm.BadRequestError with status 400, so applications catching the documented LiteLLM exception no longer handle this invalid request. The same regression affects adecisions. Move these adapter validations inside the ValidationError conversion boundary in prepare_call, or wrap them here

A local comparison against the base implementation reconstructed from the supplied diff returned base: BadRequestError 400 and head: ValidationError None

_prepare_decisions_call evaluates _request_fields(...) before entering prepare_call, so its validation-error handler cannot catch these exceptions

Review by greptile · Reviewed d5b0ad087ce5

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.

Same as the thread above, main.py is byte-identical to main's #45846. Moving the adapters inside prepare_call lands on main first, then gets picked

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

1 issue from previous review remains unresolved.

Fix All in Cursor

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit d5b0ad0. Configure here.

@mateo-berri
mateo-berri merged commit 7b40637 into rc/1.106.0 Oct 11, 2026
8 of 9 checks passed
@mateo-berri
mateo-berri deleted the litellm_backport_decisions_ui_rc_1_106_0 branch October 11, 2026 05:15
@moe-berri

Copy link
Copy Markdown
Contributor

@litereview code-review

@litereview

litereview Bot commented Oct 11, 2026

Copy link
Copy Markdown

Review not started

LiteReview only reviews open pull requests.

This branch is waiting to be deployed

1 waiting deployment
e2e-changed — d5b0ad08 Waiting Oct 11, 2026 by devin-ai-integration[bot] via oauth #4534
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.

3 participants