Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e3956bf
feat(decisions): add the OpenAI Decisions spec types and the System O…
kerry-berri Oct 7, 2026
09be315
refactor(decisions): share one DecisionsModel config and require mode…
kerry-berri Oct 7, 2026
7e20e92
refactor(decisions): rename the shared pydantic parent to DecisionsOb…
kerry-berri Oct 7, 2026
edbd1d8
refactor(decisions): dispatch /v1/decisions through provider configs …
kerry-berri Oct 7, 2026
e3fda06
fix(decisions): map OpenRouter connection failures to APIConnectionEr…
kerry-berri Oct 7, 2026
84ec670
feat(decisions): switch /v1/decisions to the OpenAI Decisions schema
kerry-berri Oct 7, 2026
9b8882d
test(decisions): build the usage the OpenAI response schema now requires
kerry-berri Oct 7, 2026
353e4fc
fix(lens): ask signal questions through the OpenAI Decisions schema
kerry-berri Oct 7, 2026
d96813e
Merge origin/main into litellm_decisions_provider_config
mateo-berri Oct 8, 2026
d3163aa
feat(decisions): refuse safety_identifier for System One providers un…
mateo-berri Oct 8, 2026
cd67b41
Merge origin/main into litellm_decisions_provider_config
mateo-berri Oct 8, 2026
d517912
feat(decisions): refuse safety_identifier on providers that cannot ta…
mateo-berri Oct 8, 2026
bca1ae2
Merge remote-tracking branch 'origin/main' into litellm_decisions_pro…
mateo-berri Oct 8, 2026
0f6c9c3
Merge remote-tracking branch 'origin/main' into litellm_decisions_pro…
mateo-berri Oct 8, 2026
6d6eb9d
Merge remote-tracking branch 'origin/main' into HEAD
mateo-berri Oct 9, 2026
e547414
test(decisions): cover hosted_vllm in the safety_identifier and per-p…
mateo-berri Oct 9, 2026
6ed3e65
fix(decisions): check a System One request's safety_identifier agains…
mateo-berri Oct 9, 2026
48fd742
fix(decisions): drop a non-string safety_identifier under drop_params
mateo-berri Oct 9, 2026
21d0833
fix(decisions): let /v1/decisions drop a non-string safety_identifier…
mateo-berri Oct 9, 2026
883302e
Merge remote-tracking branch 'origin/main' into HEAD
mateo-berri Oct 9, 2026
4c3a604
Merge remote-tracking branch 'origin/main' into HEAD
mateo-berri Oct 10, 2026
1008005
refactor(decisions): return the unsupported safety_identifier as a va…
mateo-berri Oct 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 59 additions & 7 deletions litellm/decisions/main.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from collections.abc import Mapping, Sequence
from dataclasses import dataclass, field
from dataclasses import dataclass, field, replace
from typing import Final, TypeAlias

import httpx
from pydantic import TypeAdapter, ValidationError
from pydantic import ConfigDict, TypeAdapter, ValidationError
from typing_extensions import assert_never

import litellm
from litellm.litellm_core_utils.core_helpers import RESPONSE_COST_HEADER
from litellm.litellm_core_utils.core_helpers import RESPONSE_COST_HEADER, normalize_drop_params
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.llms.base_llm.chat.transformation import BaseLLMException
from litellm.llms.base_llm.decisions.transformation import (
Expand Down Expand Up @@ -40,6 +40,9 @@

_SYSTEMONE_REQUEST_ADAPTER: Final[TypeAdapter[DecisionsRequestBody]] = TypeAdapter(DecisionsRequestBody)
_OPENAI_REQUEST_ADAPTER: Final[TypeAdapter[OpenAIDecisionRequestBody]] = TypeAdapter(OpenAIDecisionRequestBody)
_SAFETY_IDENTIFIER_ADAPTER: Final[TypeAdapter[str | None]] = TypeAdapter(
str | None, config=ConfigDict(title="safety_identifier")
)
_HANDLER: Final = BaseLLMHTTPHandler()


Expand Down Expand Up @@ -96,16 +99,46 @@ def _validate_request(
)


def _ir_request(request: DecisionsRequestFormat) -> DecisionsIRRequest:
def _ir_request(request: DecisionsRequestFormat, safety_identifier: str | None) -> DecisionsIRRequest:
match request:
case DecisionsRequestBody():
return systemone_request_to_ir(request)
return replace(systemone_request_to_ir(request), safety_identifier=safety_identifier)
case OpenAIDecisionRequestBody():
return openai_request_to_ir(request)
case _:
assert_never(request)


def _drops_params(kwargs: Mapping[str, object]) -> bool:
return litellm.drop_params is True or normalize_drop_params(kwargs.get("drop_params")) is True


def _request_safety_identifier(safety_identifier: object, kwargs: Mapping[str, object]) -> str | None:
if isinstance(safety_identifier, str) or not _drops_params(kwargs):
return _SAFETY_IDENTIFIER_ADAPTER.validate_python(safety_identifier)
return None
Comment thread
cursor[bot] marked this conversation as resolved.


@dataclass(frozen=True, slots=True)
class _UnsupportedSafetyIdentifier:
pass


def _provider_ir_request(
request: DecisionsRequestFormat,
*,
safety_identifier: str | None,
provider_config: BaseDecisionsConfig,
kwargs: Mapping[str, object],
) -> DecisionsIRRequest | _UnsupportedSafetyIdentifier:
ir_request: Final = _ir_request(request, safety_identifier)
if ir_request.safety_identifier is None or provider_config.supports_safety_identifier:
return ir_request
Comment thread
cursor[bot] marked this conversation as resolved.
if _drops_params(kwargs):
return replace(ir_request, safety_identifier=None)
return _UnsupportedSafetyIdentifier()


def _prepare_call(
*,
model: str,
Expand Down Expand Up @@ -144,8 +177,12 @@ def _prepare_call(
except ValueError as error:
raise litellm.BadRequestError(message=str(error), model=model, llm_provider=provider) from error
try:
request_safety_identifier: Final = _request_safety_identifier(safety_identifier, kwargs)
request: Final = _validate_request(
state=state, questions=questions, decision_input=decision_input, safety_identifier=safety_identifier
state=state,
questions=questions,
decision_input=decision_input,
safety_identifier=request_safety_identifier,
)
except ValidationError as error:
raise litellm.BadRequestError(
Expand All @@ -169,7 +206,22 @@ def _prepare_call(
llm_provider=provider,
)

ir_request: Final = _ir_request(request)
ir_request: Final = _provider_ir_request(
request,
safety_identifier=request_safety_identifier,
provider_config=provider_config,
kwargs=kwargs,
)
if isinstance(ir_request, _UnsupportedSafetyIdentifier):
raise litellm.UnsupportedParamsError(
message=(
f"{provider} does not support parameters: ['safety_identifier'], for model={model}. "
"To drop these, set `litellm.drop_params=True` or for proxy:\n\n"
"`litellm_settings:\n drop_params: true`\n"
),
model=model,
llm_provider=provider,
)
body: Final = provider_config.transform_decisions_request(
model=canonical_model, request=ir_request, custom_llm_provider=provider
)
Expand Down
1 change: 1 addition & 0 deletions litellm/llms/base_llm/decisions/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ class BaseDecisionsConfig(ABC):
api_key_env: tuple[str, ...] = ()
api_base_env: tuple[str, ...] = ()
api_key_required: bool = True
supports_safety_identifier: bool = False
health_check_questions: Mapping[str, Mapping[str, object]] = MappingProxyType(
{"reachable": MappingProxyType({"type": "noul", "instructions": "Is the service reachable?"})}
)
Expand Down
1 change: 1 addition & 0 deletions litellm/llms/openai/decisions/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ def ir_to_openai_response(

class OpenAIDecisionsConfig(BaseDecisionsConfig):
path = "/v1/decisions"
supports_safety_identifier = True

def get_default_api_base(self) -> str | None:
return "https://api.openai.com"
Expand Down
7 changes: 6 additions & 1 deletion litellm/proxy/decisions_endpoints/endpoints.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from collections.abc import Mapping
from types import MappingProxyType
from typing import Annotated, Final

from fastapi import APIRouter, Depends, Request, Response
Expand Down Expand Up @@ -38,6 +39,10 @@ async def _invalid_request(
)


def _fields_checked_before_routing(data: Mapping[str, object]) -> Mapping[str, object]:
return MappingProxyType({key: value for key, value in data.items() if key != "safety_identifier"})


async def _request_data(request: Request, user_api_key_dict: UserAPIKeyAuth) -> dict[str, object]:
body: Final = await request.body()
try:
Expand Down Expand Up @@ -75,7 +80,7 @@ async def _process_decisions(

data: Final = await _request_data(request, user_api_key_dict)
try:
body_adapter.validate_python(data)
body_adapter.validate_python(_fields_checked_before_routing(data))
except ValidationError as error:
raise await _invalid_request(raw_data=data, error=error, user_api_key_dict=user_api_key_dict)
general_settings: Final = _GENERAL_SETTINGS_ADAPTER.validate_python(proxy_general_settings)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,6 @@ def test_an_openai_format_request_at_v1_decisions_reaches_the_serving_endpoint_a
"model": deployment,
"input": _OPENAI_INPUT,
"questions": [_OPENAI_TIER_QUESTION],
"safety_identifier": "end-user-1",
},
)
assert response.status_code == 200, response.text
Expand Down
Loading
Loading