Skip to content

feat(decisions): backport the decisions and Jev follow-ups to rc/1.105.0 without the playground - #45905

Merged
mateo-berri merged 19 commits into
rc/1.105.0from
litellm_backport_decisions_rc_1_105_0
Oct 11, 2026
Merged

mateo-berri merged 19 commits into
rc/1.105.0from
litellm_backport_decisions_rc_1_105_0

Conversation

@devin-ai-integration

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

Copy link
Copy Markdown
Contributor

TLDR

Backports the decisions and Jev work merged on main since #45189 onto rc/1.105.0, without the decisions playground, so the 1.105.0 rc serves every decisions provider and SDK shape the launch ships with

Problem this solves:

  • rc/1.105.0 only has the openai, perplexity, typesafe, openrouter, cloudflare and strands_decider decisions providers
  • It has no hosted_vllm or azure_ai Microsoft-Decision-1 provider, no AgentCore runtime ARN support, no litellm.systemone
  • OpenRouter decisions calls are billed from the cost map instead of the provider-reported usage.cost
  • TypeSafe and Strands Decider are missing from the Add Model provider list, and Swagger UI does not describe the two decisions routes

How it solves it:

User Flow

Before: a proxy admin on 1.105.0 cannot route a decisions request to a hosted vLLM or Azure decider

  1. They add hosted_vllm/my-decider or azure_ai/Microsoft-Decision-1 to the config and start the proxy
  2. They send POST http://localhost:4000/v1/decisions with that model, an input and yes/no questions
  3. They get HTTP 400 "Unknown Decisions provider", and the Add Model page has no TypeSafe or Strands Decider provider

After: the same request is answered, and the new providers show up everywhere the old ones did

  1. They add the same models to the config and start the proxy
  2. The same POST http://localhost:4000/v1/decisions returns HTTP 200 with one answer per question and a usage block, billed at the line's price for that model
  3. http://localhost:4000/ui/?page=models lists TypeSafe and Strands Decider under Add Model, and http://localhost:4000/#/v1/decisions in Swagger UI documents both routes
  4. litellm.systemone(...) and litellm.decisions(...) work from the SDK with the same shapes as main

Picks

Skipped on purpose

Adaptations on this line

Testing

  • tests/unit/decisions, tests/unit/systemone, tests/unit/proxy/decisions_endpoints, tests/unit/proxy/public_endpoints, the strands_decider, openrouter, azure_ai and hosted_vllm provider tests, tests/unit/proxy/auth/test_auth_utils.py and tests/unit/test_cost_map_guard.py: 146 decisions and systemone tests plus the provider suites pass on this branch
  • provider_info_helpers.test.tsx: 76 passed, no type errors
  • ruff reports no new findings on the touched Python files against rc/1.105.0
  • The live before and after on a proxy built from this branch is the /ready follow-along video, linked from the Slack READY ping

Screenshots / Proof of Fix

Live /qa leg at this tip (fcbb022) against the merge base (495288a): two proxies booted from clean worktrees with --num_workers 2, their own Postgres and Redis, real TypeSafe, OpenRouter and Azure AI Foundry keys from the shared vault, real spend. Full command and output log in the QA record kept with the run; the flips a user sees:

# [C1] POST /v1/decisions model=typesafe/jev-1.13.0 with safety_identifier
BEFORE: HTTP 200 (param silently dropped)   x-litellm-response-cost: 1.2306e-05
AFTER:  HTTP 400 litellm.UnsupportedParamsError: typesafe does not support parameters: ['safety_identifier'] ...
# [C2] same body on a deployment with drop_params: true
BEFORE: HTTP 200   AFTER: HTTP 200

# [D] POST /v1/decisions model=openrouter/typesafe/jev-1.13 (billing from usage.cost)
BEFORE: HTTP 200 (no x-litellm-response-cost header, spend log 0)
AFTER:  HTTP 200 x-litellm-response-cost: 1.4532e-05 (spend log 1.4532e-05)

# [G1] POST /v1/systemone model=azure_ai/decision-1 (real Microsoft-Decision-1 deployment)
BEFORE: HTTP 400 Unknown Decisions provider 'azure_ai'. Supported providers: perplexity, typesafe, openrouter, cloudflare, strands_decider, openai
AFTER:  HTTP 200 {"model":"microsoft-decision-1","answers":{...}}
# [G2] same on POST /v1/decisions: BEFORE 400, AFTER 200

# [F] GET /v1/model/info azure_ai/Microsoft-Decision-1
BEFORE: input_cost_per_token null, mode null
AFTER:  input_cost_per_token 4.2e-08, output_cost_per_token 0.0, mode evaluation

# [K] POST /v1/decisions with agentcore_runtime_session_id in the body
BEFORE: HTTP 200 (forwarded)   AFTER: HTTP 401 not allowed in request body

# [L] GET /openapi.json, POST /v1/decisions
BEFORE: summary "Decisions", no description, no request example
AFTER:  summary "Ask questions about an input (OpenAI Decisions format)", description, example, schema props input/model/questions/safety_identifier

# [M] GET /guardrails/ui/add_guardrail_settings decision_model_providers (the relocated helper)
BEFORE: [perplexity, typesafe, openrouter, cloudflare, strands_decider, openai]
AFTER:  [openai, openrouter, azure_ai, perplexity, typesafe, strands_decider, cloudflare, hosted_vllm]

# Admin UI Add Model provider dropdown (Playwright, before :44105 / after :44106)
BEFORE: no TypeSafe or Strands Decider entries (GET /public/providers/fields: 0 matches)
AFTER:  "TypeSafe" and "Strands Decider" entries present with their default model placeholders

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; its request-body rejection is proven above)

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)

Type

🆕 New Feature

Caveats (if any)

Medium

  • No Databricks decisions provider on this line, so databricks/... decisions requests still return the unknown provider 400
  • The OpenRouter Responses API still bills from the cost map on this line, only decisions and pass-through use usage.cost

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

devin-ai-integration Bot and others added 18 commits October 11, 2026 00:30
…ne translation (#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)
…and the shared HTTP handler (#45130)

* refactor(decisions): dispatch /v1/decisions through provider configs and the shared HTTP handler

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

* fix(decisions): map OpenRouter connection failures to APIConnectionError and drop explanatory docstrings

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

* fix(decisions): keep APIConnectionError for unreachable deployments on every provider

`_map_upstream_exception` now maps a status `_handle_error` synthesized for a
non-HTTP failure to `APIConnectionError` before `exception_type` runs, so a
Perplexity (openai-compatible) deployment that cannot be reached no longer
answers `InternalServerError` where main answers `APIConnectionError`.

Audit cells: the unreachable-deployment integration cell is parametrized over
the five providers, OpenRouter connection failures are pinned on chat, stream,
embeddings, decisions and the SDK, a deployment whose provider has no decisions
config gets the 400 naming every supported provider, and a unit cell pins the
mapper alone. The first docstring sentence of the five translation bases is
dropped.

---------

Co-authored-by: kerry <kerry@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
(cherry picked from commit 0e09e99)
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…45346)

Co-authored-by: yuneng <yuneng@berri.ai>
(cherry picked from commit 6e54dce)
…list (#45335)

* feat(ui): add TypeSafe and Strands Decider to the Add Model provider list

* test(public_endpoints): type the provider fields helper against the route's model

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
(cherry picked from commit 3a3b8ca)
…pes left dead by #45214 (#45442)

#45214 routed both decision formats through the shared decisions IR in
litellm/llms/base_llm/decisions/transformation.py and litellm/types/decisions.py.
That left litellm/llms/base_llm/decisions/systemone.py (to_system_one_request,
question_keys, to_decisions_response, the SystemOne* models and their adapter)
and litellm/types/openai_decisions.py with no importer outside their own unit
tests, so both modules and both test files go.

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
(cherry picked from commit 0e48048)
* feat(decisions): add hosted_vllm provider

Adds HostedVLLMDecisionsConfig so hosted_vllm/<served model> works on
POST /v1/systemone and POST /v1/decisions against a vLLM server that
serves the System One body (vllm-project/vllm#59299, after v0.31.0).
vLLM only supports choice questions and rejects others, so the base
decisions config gains a per-provider health_check_questions attribute
and the evaluation-mode health probe sends a choice question for
hosted_vllm while every other provider keeps the noul probe.

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

* refactor(decisions): move the hosted_vllm placeholder key to constants

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 61e5f2d)
Price-Sync: litellm-providers

Co-authored-by: berriai-litellm-provider-info-sync[bot] <328147090+berriai-litellm-provider-info-sync[bot]@users.noreply.github.com>
(cherry picked from commit de6a071)
…e.cost and always allow reasoning params (#45640) [partial: decisions and pass-through billing only]

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 465f20f)
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…5673)

* feat(azure_ai): support Microsoft-Decision-1 on the decisions API

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

* fix(azure_ai): strip project path and API suffix together in decisions base

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 2fcc780)
…ke it unless drop_params drops it (#44955)

* 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>

* refactor(decisions): dispatch /v1/decisions through provider configs and the shared HTTP handler

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

* fix(decisions): map OpenRouter connection failures to APIConnectionError and drop explanatory docstrings

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

* feat(decisions): switch /v1/decisions to the OpenAI Decisions schema

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

* test(decisions): build the usage the OpenAI response schema now requires

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

* fix(lens): ask signal questions through the OpenAI Decisions schema

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

* feat(decisions): refuse safety_identifier for System One providers unless drop_params

* feat(decisions): refuse safety_identifier on providers that cannot take it unless drop_params drops it

* test(decisions): cover hosted_vllm in the safety_identifier and per-provider wire tests

* fix(decisions): check a System One request's safety_identifier against the provider too

* fix(decisions): drop a non-string safety_identifier under drop_params

A malformed safety_identifier now follows the drop_params convention in both body shapes: it answers 400 without drop_params and is dropped before the provider call with it. A string identifier on OpenAI stays on the wire either way.

* fix(decisions): let /v1/decisions drop a non-string safety_identifier under drop_params

The route checked the whole body before routing, so a non-string
safety_identifier answered 400 even when the deployment or the body set
drop_params. The route now leaves that field to the Decisions call, which
knows every drop_params source

* refactor(decisions): return the unsupported safety_identifier as a value and raise it in _prepare_call

---------

Co-authored-by: kerry <kerry@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
(cherry picked from commit 546402c)
…t-Decision-1 (#45717)

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 674208b)
… ARN (#45678)

* feat(strands_decider): call a Strands Decider on an AgentCore runtime ARN

A strands_decider deployment whose api_base is a bedrock-agentcore runtime ARN now
calls InvokeAgentRuntime on that runtime, signed with SigV4 from the deployment's aws_*
params or the default chain, reuses one session per runtime, and maps the runtime's
200 error envelopes to 400, 503, or 500

* ci: run the strands_decider unit tests in the llms job

* fix(strands_decider): take the AgentCore session id from the deployment, never the caller

A caller could pick the runtime session through extra_headers, and every new id starts another microVM. The session id now comes from the deployment's agentcore_runtime_session_id (banned in request bodies) or the ARN default, and caller-supplied host, x-amz-*, and x-amzn-bedrock-agentcore-runtime-* headers are not forwarded

* fix(strands_decider): map runtime error replies only on AgentCore ARN calls

* fix(strands_decider): sign plain decisions calls inline, off the AWS signing executor

* test(strands_decider): cover AgentCore runtime ARN calls in integration wire tests

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
(cherry picked from commit ed9c31c)
…45838)

* docs(proxy): describe /v1/decisions and /v1/systemone in Swagger UI

Add summaries, descriptions, inlined request body schemas with examples and typed 200 responses to the
decision routes and their aliases, so the OpenAPI document and the lazy snapshot show both request formats

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

* fix(proxy): cap _inlined schema walk depth for the recursion check

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 bc2cc42)
…-reported cost (#45853)

PR #45667 removed openrouter/typesafe/jev-1.13 from the cost map after #45640 moved OpenRouter decisions billing to usage.cost. The OpenAI-format wire test still read the removed row and raised KeyError on CircleCI integration-providers

Co-authored-by: mateo <mateo@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
(cherry picked from commit 5894968)
…enAI shape (#45846)

* feat(sdk): add litellm.systemone and trim litellm.decisions to the OpenAI shape

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

* ci: run tests/unit/systemone in the core-utils unit shard

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

* feat(sdk): keep litellm.decisions(state=...) working with a DeprecationWarning

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

* test: make tests/unit/systemone a package

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

* feat(sdk): surface the litellm.decisions(state=...) deprecation as a UserWarning

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

---------

Co-authored-by: kerry <kerry@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
(cherry picked from commit 25d9b05)
…o rc/1.105.0

schema.d.ts is regenerated with npm run gen:api instead of hand-merged, the Strands logo the Add Model list imports is copied from main, and the picked tests get the import order and Final import this line's ruff asks for

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…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>
@devin-ai-integration

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

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

…ers to litellm.decisions.call

The decision model guardrail backport (#45894) landed on the line after this branch
forked and imports supported_decisions_providers from litellm.decisions.main, which
#45846 replaces here. Match main by exposing it from litellm.decisions.call and point
the guardrail endpoint and its test at that module. schema.d.ts and the lazy OpenAPI
snapshot are regenerated on the merged tree

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit fcbb022. Configure here.

Comment thread litellm/decisions/main.py
DECISIONS_ENDPOINTS: Final[Mapping[str, DecisionsProviderConfig]] = MappingProxyType(
{
"perplexity": PERPLEXITY_DECISIONS_ENDPOINT,
"typesafe": TYPESAFE_DECISIONS_ENDPOINT,

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.

State field misroutes OpenAI decisions

Medium Severity

_request_fields treats any state key in kwargs as the deprecated System One format. The proxy forwards the full body into adecisions, and OpenAIDecisionRequestBody allows extra properties, so a /v1/decisions request that also includes state is handled as System One. The OpenAI questions list then fails the Mapping adapter and raises an uncaught ValidationError instead of a 400. Mixed input and state on litellm.decisions is no longer rejected.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit fcbb022. Configure here.

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.

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

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

@litereview greptile code-review

@litereview

litereview Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

✅ greptile

Completed · Some checks not run.

Score: 4/5

The provider and routing backport looks ready apart from one SDK validation regression. I ran 201 focused tests successfully and reproduced the exception-contract failure below

Checks and execution details
  • Passed · Repository and knowledge-base context (required): Read applicable AGENTS.md files, the supplied PR metadata and diff, and Greptile's index and model-request-execution documentation
  • Passed · Focused Python tests (optional): 201 passed: decisions SDK, System One SDK, Strands AgentCore transformation, proxy decisions endpoints, OpenAI decisions transformation, OpenRouter chat transformation, and cost-map guard. Installed dependencies with uv sync --frozen --extra proxy --group dev, then installed pinned prisma==0.11.0 to resolve proxy test collection
  • Failed · SDK validation regression reproducer (optional): An invalid questions list raised Pydantic ValidationError instead of LiteLLM BadRequestError; the exception-contract assertion failed
  • Not run · Live provider verification (optional): No live Azure, hosted vLLM, or AgentCore calls; provider verification used the focused tests

View review · Reviewed fcbb02231b28

@litereview

litereview Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

✅ code-review

Completed · Some checks not run.

I'm the standards reviewer for #45905. I found no hard breaches of the documented standards in the code this PR adds. There is one borderline breach of the comment rule, several smells (all judgement calls), and one low-severity correctness bug.

Standards

Documented standards

  • Comment rule, borderline (AGENTS.md: comments are not allowed unless "absolutely necessary"):
    • The new class docstring on AzureAIDecisionsConfig only repeats what the class says.
    • The rewritten docstring on OpenrouterConfig.get_supported_openai_params does the same.
    • These are fine:
      • The inline_request_body docstring explains a non-obvious Swagger $ref constraint.
      • The auth_utils.py comment gives a security reason.
      • The endpoint docstrings are Swagger input, which counts as tool input.
      • The fixture docstrings cite where the data came from and the date, which AGENTS.md asks for.
  • Unrelated changes in the same diff: test_custom_openapi_spec.py reformats existing @​patch blocks that have nothing to do with this change. The doubles on our own code were already there, so this isn't a new breach.

Smells (judgement calls)

  • Duplicated Code:
    • send/asend in litellm/decisions/call.py are the same apart from await.
    • So are systemone/asystemone, and the handler's decisions/adecisions.
    • The next(member for member in LlmProviders if member.value == ...) lookup is copied in call.py:97 and health_check_helpers.py:41.
  • Repeated Switches: get_provider_decisions_config is an eight-branch if provider == ... chain. A provider→class map would let the lookup above share it.
  • Primitive Obsession / duplicated knowledge: the TypeSafe passthrough handler checks custom_llm_provider == "openrouter". That repeats what OpenRouterDecisionsConfig.provider_reported_cost already knows.
  • Data Clumps / Middle Man: api_key, api_base, timeout, custom_llm_provider, extra_headers, kwargs move together through _prepare_systemone_call and _prepare_decisions_call into prepare_call. Both wrappers mostly just pass them on.
  • Shotgun Surgery: adding the "asystemone" call type took matching list edits across six files, nine places in all.
  • Refused Bequest: SystemOneRequestFields.validate(self, safety_identifier) never uses its argument.
  • Speculative Generality: the empty EVALUATION_OUTPUT_PRICE_SOURCES is threaded through as a keyword-only parameter of guard_failures and _file_failures, and only tests use it.
  • Mysterious Name: _fields_checked_before_routing actually removes safety_identifier from the request data. Something like _without_safety_identifier would say that.

Correctness

  • Low: non-JSON replies from the provider get past the decisions error handling (finding below).
  • Behaviour change in a backport: OpenRouter chat now offers reasoning_effort and thinking for every model. Worth confirming this was meant to ride along.

Spec

Out of scope here: I was only assigned the Standards axis.

Totals: Standards has one borderline breach of the comment rule, eight smells, and one low-severity correctness bug. The worst item is the validate_json call sitting outside the try block.

Checks and execution details
  • Passed · Read standards sources (required): Read AGENTS.md, tests/AGENTS.md, tests/unit/AGENTS.md, tests/integration/AGENTS.md and ui/litellm-dashboard/AGENTS.md, and grepped CONTRIBUTING.md and ARCHITECTURE.md for rules.
  • Passed · Manual diff review against standards and smell baseline (required): Reviewed the non-generated source and test hunks in /tmp/pr.diff and checked suspected bugs against the checkout. Skimmed schema.d.ts and the snapshot JSON.
  • Not run · Run unit tests (optional): This was a standards review done by reading code; I didn't run any tests.

View review · Reviewed fcbb02231b28

raw_response: httpx.Response,
request: DecisionsIRRequest,
) -> DecisionsIRResponse:
payload: Final[object] = _PAYLOAD_ADAPTER.validate_json(raw_response.content)

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 · Non-JSON provider replies skip the decisions error wrapper

_PAYLOAD_ADAPTER.validate_json(raw_response.content) runs before the try block. A 200 response with a non-JSON body (an HTML gateway page, say) raises a raw pydantic ValidationError instead of the BaseLLMException(status_code=500, "...returned an unexpected response") the method builds for bad payloads. The raw error then goes to litellm.exception_type through _map_upstream_exception (litellm/decisions/call.py:286). Moving the parse inside the try fixes it.

payload: Final[object] = _PAYLOAD_ADAPTER.validate_json(raw_response.content)
try:
return self.parse_response(payload, request)
except ValidationError as error:

Review by code-review · Reviewed fcbb02231b28

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.

Faithful pick, base_llm/decisions/transformation.py is byte-identical to main. Moving validate_json inside the try lands on main first, then a follow-up pick

pricing: Final = _pricing_for(candidate_model_keys)
response_cost: Final = (
input_tokens * pricing.input_cost_per_token + output_tokens * pricing.output_cost_per_token
usage.cost

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 provider-cost check duplicated as a string check in the passthrough handler

Judgement call (Primitive Obsession / Duplicated Code). Using provider-reported cost is already modelled by BaseDecisionsConfig.provider_reported_cost, which OpenRouterDecisionsConfig overrides. Here the same rule is hard-coded as custom_llm_provider == "openrouter", so the two copies can drift apart.

usage.cost
if custom_llm_provider == "openrouter" and usage.cost is not None

Review by code-review · Reviewed fcbb02231b28

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.

1.105 keeps the string check on purpose, main's provider_reported_cost path needs litellm_core_utils.hidden_params from #45640, which this line does not have

Comment thread litellm/utils.py
return litellm.CohereRerankConfig()

@staticmethod
def get_provider_decisions_config(model: str, provider: LlmProviders) -> BaseDecisionsConfig | None:

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 · Copy-pasted provider lookup plus an if-cascade provider registry

Judgement call (Repeated Switches / Duplicated Code). get_provider_decisions_config is an eight-branch if provider == ... chain. The string-to-LlmProviders lookup that feeds it is copied in litellm/decisions/call.py:97 and litellm/litellm_core_utils/health_check_helpers.py:41. One shared map would replace all three.

provider: Final = next((member for member in LlmProviders if member.value == custom_llm_provider), None)

Review by code-review · Reviewed fcbb02231b28

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.

Judgement call on code main carries today, plus a Databricks branch there. A provider map refactor lands on main first, this backport ships merged picks

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 · Sync and async decisions paths are copies of each other

Judgement call (Duplicated Code). send and asend are the same apart from await. The same is true of systemone/asystemone in litellm/systemone/main.py, decisions/adecisions in litellm/decisions/main.py, and the handler methods in llm_http_handler.py. The thin _prepare_*_call wrappers also just pass the same six-parameter group on to prepare_call (Middle Man / Data Clumps).

def send(call: DecisionsCall) -> DecisionsResponse | OpenAIDecisionResponse:
try:
response: Final = _HANDLER.decisions(

Review by code-review · Reviewed fcbb02231b28

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.

Judgement call, call.py is byte-identical to main. Deduplicating send and asend is a main refactor, out of scope for a release-line backport



class AzureAIDecisionsConfig(BaseDecisionsConfig):
"""Microsoft Foundry decision models (Microsoft-Decision-1), where model is the deployment name"""

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 · Docstrings that just restate the code

AGENTS.md says not to write comments unless they are absolutely necessary to explain complex business logic. The new class docstring here, and the rewritten OpenrouterConfig.get_supported_openai_params docstring ("Include reasoning parameters regardless of model-map capabilities."), only repeat what the code already says.

"""Microsoft Foundry decision models (Microsoft-Decision-1), where model is the deployment name"""

Review by code-review · Reviewed fcbb02231b28

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.

Faithful pick, the docstring is identical on main. Trimming it belongs on main first, this backport only carries merged code

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 · Map field validation failures to BadRequestError

Calling adecisions(model="typesafe/jev-1.13", input="review", questions=[7], api_key="test-key") now raises a raw Pydantic ValidationError without a status code. _request_fields validates before entering prepare_call's error mapping, so SDK callers catching litellm.BadRequestError miss this failure. The previous implementation converted request-validation failures to BadRequestError. Move these validations inside that mapping or catch and translate them here

A local reproducer failed its isinstance(error, litellm.BadRequestError) assertion; the traceback points to _OPENAI_QUESTIONS_ADAPTER.validate_python on line 36

Review by greptile · Reviewed fcbb02231b28

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.

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

@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 2616b1a into rc/1.105.0 Oct 11, 2026
15 of 18 checks passed
@mateo-berri
mateo-berri deleted the litellm_backport_decisions_rc_1_105_0 branch October 11, 2026 05:15

This branch is waiting to be deployed

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