diff --git a/gateway/routes/allowlist.py b/gateway/routes/allowlist.py
index 31c05f21b1b..3cfe9569b34 100644
--- a/gateway/routes/allowlist.py
+++ b/gateway/routes/allowlist.py
@@ -62,6 +62,8 @@ GATEWAY_PATH_PREFIXES: tuple[str, ...] = (
"/rerank",
"/v1/decisions",
"/decisions",
+ "/v1/systemone",
+ "/systemone",
"/v1/ocr",
"/ocr",
"/v1/rag/",
diff --git a/litellm/decisions/openai_transformation.py b/litellm/decisions/openai_transformation.py
new file mode 100644
index 00000000000..af2c838b9ab
--- /dev/null
+++ b/litellm/decisions/openai_transformation.py
@@ -0,0 +1,127 @@
+from collections.abc import Mapping, Sequence
+from typing import Final
+
+from typing_extensions import assert_never
+
+from litellm.types.decisions import (
+ ChoiceAnswer,
+ DecisionAnswer,
+ DecisionsResponse,
+ DecisionsUsage,
+ NoulAnswer,
+ OpenAIChoiceAnswer,
+ OpenAIChoiceProbability,
+ OpenAIChoiceQuestion,
+ OpenAIDecisionAnswer,
+ OpenAIDecisionInputMessage,
+ OpenAIDecisionQuestion,
+ OpenAIDecisionRequestBody,
+ OpenAIDecisionResponse,
+ OpenAIDecisionUsage,
+ OpenAIPredicateAnswer,
+ OpenAIPredicateQuestion,
+ OpenAIRefusalAnswer,
+ OpenAIScoreAnswer,
+ OpenAIScoreLevel,
+ OpenAIScoreProbability,
+ OpenAIScoreQuestion,
+ ScoreAnswer,
+ systemone_choice_key,
+)
+
+_OPENAI_ONLY_FIELDS: Final = frozenset({"input", "questions", "safety_identifier"})
+
+
+def _message_text(message: OpenAIDecisionInputMessage) -> str:
+ if isinstance(message.content, str):
+ return message.content
+ return "\n\n".join(part.text for part in message.content)
+
+
+def _state(decision_input: str | Sequence[OpenAIDecisionInputMessage]) -> str:
+ if isinstance(decision_input, str):
+ return decision_input
+ return "\n\n".join(_message_text(message) for message in decision_input)
+
+
+def _level_criterion(level: OpenAIScoreLevel) -> str:
+ return level.label if level.description is None else f"{level.label}: {level.description}"
+
+
+def _systemone_question(question: OpenAIDecisionQuestion) -> Mapping[str, object]:
+ match question:
+ case OpenAIPredicateQuestion():
+ return {"type": "noul", "instructions": question.instructions}
+ case OpenAIChoiceQuestion():
+ return {
+ "type": "choice",
+ "instructions": question.instructions,
+ "criteria": {systemone_choice_key(option.value): option.description for option in question.choices},
+ }
+ case OpenAIScoreQuestion():
+ return {
+ "type": "score",
+ "instructions": question.instructions,
+ "criteria": [_level_criterion(level) for level in question.levels],
+ }
+ case _:
+ assert_never(question)
+
+
+def to_systemone_request(request_data: Mapping[str, object], body: OpenAIDecisionRequestBody) -> Mapping[str, object]:
+ return {
+ **{key: value for key, value in request_data.items() if key not in _OPENAI_ONLY_FIELDS},
+ "state": _state(body.input),
+ "questions": {str(index): _systemone_question(question) for index, question in enumerate(body.questions)},
+ }
+
+
+def _openai_answer(question: OpenAIDecisionQuestion, answer: DecisionAnswer | None) -> OpenAIDecisionAnswer:
+ match question, answer:
+ case OpenAIPredicateQuestion(), NoulAnswer():
+ return OpenAIPredicateAnswer(name=question.name, probability=answer.noul)
+ case OpenAIChoiceQuestion(), ChoiceAnswer():
+ typed_values: Final = {systemone_choice_key(option.value): option.value for option in question.choices}
+ return OpenAIChoiceAnswer(
+ name=question.name,
+ choice=typed_values.get(answer.choice, answer.choice),
+ probabilities=tuple(
+ OpenAIChoiceProbability(
+ value=option.value,
+ probability=answer.probabilities.get(systemone_choice_key(option.value), 0.0),
+ )
+ for option in question.choices
+ ),
+ confidence=answer.confidence,
+ )
+ case OpenAIScoreQuestion(), ScoreAnswer():
+ return OpenAIScoreAnswer(
+ name=question.name,
+ score=answer.score,
+ probabilities=tuple(
+ OpenAIScoreProbability(
+ value=index, label=level.label, probability=answer.probabilities.get(str(index), 0.0)
+ )
+ for index, level in enumerate(question.levels)
+ ),
+ confidence=answer.confidence,
+ )
+ case _:
+ return OpenAIRefusalAnswer(name=question.name)
+
+
+def to_openai_response(
+ response: DecisionsResponse, questions: Sequence[OpenAIDecisionQuestion], requested_model: str
+) -> OpenAIDecisionResponse:
+ usage: Final = response.usage or DecisionsUsage()
+ return OpenAIDecisionResponse(
+ model=response.model or requested_model,
+ answers=tuple(
+ _openai_answer(question, response.answers.get(str(index))) for index, question in enumerate(questions)
+ ),
+ usage=OpenAIDecisionUsage(
+ input_tokens=usage.input_tokens,
+ output_tokens=usage.output_tokens,
+ total_tokens=usage.input_tokens + usage.output_tokens,
+ ),
+ )
diff --git a/litellm/proxy/_lazy_features.py b/litellm/proxy/_lazy_features.py
index f018029058f..517c0916e13 100644
--- a/litellm/proxy/_lazy_features.py
+++ b/litellm/proxy/_lazy_features.py
@@ -267,7 +267,7 @@ LAZY_FEATURES: Final[tuple[LazyFeature, ...]] = (
LazyFeature(
name="decisions",
module_path="litellm.proxy.decisions_endpoints.endpoints",
- path_prefixes=("/v1/decisions", "/decisions"),
+ path_prefixes=("/v1/decisions", "/decisions", "/v1/systemone", "/systemone"),
),
LazyFeature(
name="claude_code_marketplace",
diff --git a/litellm/proxy/_lazy_openapi_snapshot.json b/litellm/proxy/_lazy_openapi_snapshot.json
index 06f1a666a70..9322fb77615 100644
--- a/litellm/proxy/_lazy_openapi_snapshot.json
+++ b/litellm/proxy/_lazy_openapi_snapshot.json
@@ -9521,6 +9521,30 @@
]
}
},
+ "/systemone": {
+ "post": {
+ "operationId": "systemone_systemone_post",
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ },
+ "description": "Successful Response"
+ }
+ },
+ "security": [
+ {
+ "APIKeyHeader": []
+ }
+ ],
+ "summary": "Systemone",
+ "tags": [
+ "decisions"
+ ]
+ }
+ },
"/v1/decisions": {
"post": {
"operationId": "decisions_v1_decisions_post",
@@ -9544,6 +9568,30 @@
"decisions"
]
}
+ },
+ "/v1/systemone": {
+ "post": {
+ "operationId": "systemone_v1_systemone_post",
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ },
+ "description": "Successful Response"
+ }
+ },
+ "security": [
+ {
+ "APIKeyHeader": []
+ }
+ ],
+ "summary": "Systemone",
+ "tags": [
+ "decisions"
+ ]
+ }
}
}
},
diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py
index ff36f3f78db..bc27b2352a9 100644
--- a/litellm/proxy/_types.py
+++ b/litellm/proxy/_types.py
@@ -488,6 +488,8 @@ class LiteLLMRoutes(enum.Enum):
"/v1/search/{search_tool_name}",
"/decisions",
"/v1/decisions",
+ "/systemone",
+ "/v1/systemone",
# OCR
"/ocr",
"/v1/ocr",
diff --git a/litellm/proxy/agent_endpoints/auth/managed_authorization.py b/litellm/proxy/agent_endpoints/auth/managed_authorization.py
index 5d8287227a5..64e50cd74e7 100644
--- a/litellm/proxy/agent_endpoints/auth/managed_authorization.py
+++ b/litellm/proxy/agent_endpoints/auth/managed_authorization.py
@@ -30,6 +30,7 @@ _MANAGED_MODEL_ROUTES: Final = frozenset(
"moderations",
"rerank",
"decisions",
+ "systemone",
"ocr",
),
)
@@ -74,6 +75,7 @@ _MODEL_ROUTE_KINDS: Final[
"/audio/speech": "speech",
"/rerank": "body",
"/decisions": "body",
+ "/systemone": "body",
"/messages/count_tokens": "body",
":countTokens": "path",
}
diff --git a/litellm/proxy/decisions_endpoints/endpoints.py b/litellm/proxy/decisions_endpoints/endpoints.py
index e05d37963cb..265dbbb2957 100644
--- a/litellm/proxy/decisions_endpoints/endpoints.py
+++ b/litellm/proxy/decisions_endpoints/endpoints.py
@@ -1,39 +1,64 @@
+from collections.abc import Mapping
from typing import Annotated, Final
from fastapi import APIRouter, Depends, Request, Response
from fastapi.responses import ORJSONResponse # pyright: ignore[reportDeprecated] # required endpoint contract
from pydantic import TypeAdapter, ValidationError
+from litellm.decisions.openai_transformation import to_openai_response, to_systemone_request
from litellm.exceptions import BadRequestError
from litellm.proxy.auth.user_api_key_auth import UserAPIKeyAuth, user_api_key_auth
-from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing
-from litellm.types.decisions import DecisionsRequestBody
+from litellm.proxy.common_request_processing import (
+ ProxyBaseLLMRequestProcessing,
+ attach_guardrail_information,
+ include_guardrail_response_requested,
+)
+from litellm.types.decisions import DecisionsRequestBody, DecisionsResponse, OpenAIDecisionRequestBody
router: Final = APIRouter()
_REQUEST_DATA_ADAPTER: Final[TypeAdapter[dict[str, object]]] = TypeAdapter(dict[str, object])
_DECISIONS_REQUEST_BODY_ADAPTER: Final[TypeAdapter[DecisionsRequestBody]] = TypeAdapter(DecisionsRequestBody)
+_OPENAI_DECISION_REQUEST_BODY_ADAPTER: Final[TypeAdapter[OpenAIDecisionRequestBody]] = TypeAdapter(
+ OpenAIDecisionRequestBody
+)
+_DECISIONS_RESPONSE_ADAPTER: Final[TypeAdapter[DecisionsResponse]] = TypeAdapter(DecisionsResponse)
_GENERAL_SETTINGS_ADAPTER: Final[TypeAdapter[dict[str, object]]] = TypeAdapter(dict[str, object])
_OPTIONAL_STRING_ADAPTER: Final[TypeAdapter[str | None]] = TypeAdapter(str | None)
_OPTIONAL_FLOAT_ADAPTER: Final[TypeAdapter[float | None]] = TypeAdapter(float | None)
-@router.post(
- "/v1/decisions",
- dependencies=[Depends(user_api_key_auth)],
- response_class=ORJSONResponse, # pyright: ignore[reportDeprecated] # required endpoint contract
- tags=["decisions"],
-)
-@router.post(
- "/decisions",
- dependencies=[Depends(user_api_key_auth)],
- response_class=ORJSONResponse, # pyright: ignore[reportDeprecated] # required endpoint contract
- tags=["decisions"],
-)
-async def decisions(
+async def _invalid_request(
+ raw_data: Mapping[str, object], error: ValidationError, user_api_key_dict: UserAPIKeyAuth
+) -> Exception:
+ from litellm.proxy.proxy_server import proxy_logging_obj, version
+
+ return await ProxyBaseLLMRequestProcessing(data=dict(raw_data)).handle_llm_api_exception(
+ e=BadRequestError(
+ message=f"Invalid Decisions request: {error}",
+ model=str(raw_data.get("model", "")),
+ llm_provider="",
+ ),
+ user_api_key_dict=user_api_key_dict,
+ proxy_logging_obj=proxy_logging_obj,
+ version=version,
+ )
+
+
+async def _request_data(request: Request, user_api_key_dict: UserAPIKeyAuth) -> dict[str, object]:
+ body: Final = await request.body()
+ try:
+ return _REQUEST_DATA_ADAPTER.validate_json(body)
+ except ValidationError as error:
+ raise await _invalid_request(raw_data={}, error=error, user_api_key_dict=user_api_key_dict)
+
+
+async def _process_systemone(
request: Request,
fastapi_response: Response,
- user_api_key_dict: Annotated[UserAPIKeyAuth, Depends(user_api_key_auth)],
-):
+ user_api_key_dict: UserAPIKeyAuth,
+ raw_data: Mapping[str, object],
+ openai_body: OpenAIDecisionRequestBody | None,
+) -> object:
from litellm.proxy.proxy_server import (
general_settings as proxy_general_settings,
)
@@ -55,7 +80,7 @@ async def decisions(
user_temperature as proxy_user_temperature,
)
- data: Final = _REQUEST_DATA_ADAPTER.validate_json(await request.body())
+ data: Final = dict(raw_data if openai_body is None else to_systemone_request(raw_data, openai_body))
general_settings: Final = _GENERAL_SETTINGS_ADAPTER.validate_python(proxy_general_settings)
user_api_base: Final = _OPTIONAL_STRING_ADAPTER.validate_python(proxy_user_api_base)
user_model: Final = _OPTIONAL_STRING_ADAPTER.validate_python(proxy_user_model)
@@ -63,7 +88,7 @@ async def decisions(
processor: Final = ProxyBaseLLMRequestProcessing(data=data)
try:
_DECISIONS_REQUEST_BODY_ADAPTER.validate_python(data)
- return await processor.base_process_llm_request(
+ result: Final[object] = await processor.base_process_llm_request(
request=request,
fastapi_response=fastapi_response,
user_api_key_dict=user_api_key_dict,
@@ -81,18 +106,21 @@ async def decisions(
user_api_base=user_api_base,
version=version,
)
+ if openai_body is None or isinstance(result, Response):
+ return result
+ openai_response: Final = to_openai_response(
+ _DECISIONS_RESPONSE_ADAPTER.validate_python(result),
+ openai_body.questions,
+ str(data.get("model", "")),
+ )
+ request_data: Final = _REQUEST_DATA_ADAPTER.validate_python(
+ processor.data # pyright: ignore[reportUnknownMemberType] # ProxyBaseLLMRequestProcessing.data is a bare dict
+ )
+ if include_guardrail_response_requested(request_data):
+ return attach_guardrail_information(response=openai_response, request_data=request_data)
+ return openai_response
except ValidationError as error:
- bad_request_error: Final = BadRequestError(
- message=f"Invalid Decisions request: {error}",
- model=str(data.get("model", "")),
- llm_provider="",
- )
- raise await processor.handle_llm_api_exception(
- e=bad_request_error,
- user_api_key_dict=user_api_key_dict,
- proxy_logging_obj=proxy_logging_obj,
- version=version,
- )
+ raise await _invalid_request(raw_data=data, error=error, user_api_key_dict=user_api_key_dict)
except Exception as error:
raise await processor.handle_llm_api_exception(
e=error,
@@ -100,3 +128,60 @@ async def decisions(
proxy_logging_obj=proxy_logging_obj,
version=version,
)
+
+
+@router.post(
+ "/v1/systemone",
+ dependencies=[Depends(user_api_key_auth)],
+ response_class=ORJSONResponse, # pyright: ignore[reportDeprecated] # required endpoint contract
+ tags=["decisions"],
+)
+@router.post(
+ "/systemone",
+ dependencies=[Depends(user_api_key_auth)],
+ response_class=ORJSONResponse, # pyright: ignore[reportDeprecated] # required endpoint contract
+ tags=["decisions"],
+)
+async def systemone(
+ request: Request,
+ fastapi_response: Response,
+ user_api_key_dict: Annotated[UserAPIKeyAuth, Depends(user_api_key_auth)],
+):
+ return await _process_systemone(
+ request=request,
+ fastapi_response=fastapi_response,
+ user_api_key_dict=user_api_key_dict,
+ raw_data=await _request_data(request, user_api_key_dict),
+ openai_body=None,
+ )
+
+
+@router.post(
+ "/v1/decisions",
+ dependencies=[Depends(user_api_key_auth)],
+ response_class=ORJSONResponse, # pyright: ignore[reportDeprecated] # required endpoint contract
+ tags=["decisions"],
+)
+@router.post(
+ "/decisions",
+ dependencies=[Depends(user_api_key_auth)],
+ response_class=ORJSONResponse, # pyright: ignore[reportDeprecated] # required endpoint contract
+ tags=["decisions"],
+)
+async def decisions(
+ request: Request,
+ fastapi_response: Response,
+ user_api_key_dict: Annotated[UserAPIKeyAuth, Depends(user_api_key_auth)],
+):
+ raw_data: Final = await _request_data(request, user_api_key_dict)
+ try:
+ openai_body: Final = _OPENAI_DECISION_REQUEST_BODY_ADAPTER.validate_python(raw_data)
+ except ValidationError as error:
+ raise await _invalid_request(raw_data=raw_data, error=error, user_api_key_dict=user_api_key_dict)
+ return await _process_systemone(
+ request=request,
+ fastapi_response=fastapi_response,
+ user_api_key_dict=user_api_key_dict,
+ raw_data=raw_data,
+ openai_body=openai_body,
+ )
diff --git a/litellm/types/decisions.py b/litellm/types/decisions.py
index 8bc535ffebe..b543b3b32df 100644
--- a/litellm/types/decisions.py
+++ b/litellm/types/decisions.py
@@ -1,5 +1,5 @@
from collections.abc import Mapping, Sequence
-from typing import Annotated, Literal, TypeAlias
+from typing import Annotated, Final, Literal, TypeAlias
from pydantic import ConfigDict, Field, PrivateAttr, model_validator, with_config
from typing_extensions import ReadOnly, Required, TypedDict
@@ -8,6 +8,7 @@ from litellm.types.llms.base import LiteLLMPydanticObjectBase
DecisionsJSON: TypeAlias = str | Mapping[str, object] | Sequence[object]
NoulCriteria: TypeAlias = Mapping[Literal["true", "false"], DecisionsJSON | None]
+MAX_DECISION_QUESTIONS: Final = 128
class NoulQuestion(LiteLLMPydanticObjectBase):
@@ -47,7 +48,7 @@ DecisionQuestion: TypeAlias = Annotated[
DecisionQuestionMap: TypeAlias = Annotated[
Mapping[Annotated[str, Field(min_length=1)], DecisionQuestion],
- Field(min_length=1, max_length=128),
+ Field(min_length=1, max_length=MAX_DECISION_QUESTIONS),
]
@@ -128,3 +129,169 @@ class DecisionsResponse(LiteLLMPydanticObjectBase):
def set_hidden_params(self, params: Mapping[str, object]) -> None:
self._hidden_params.update(params)
+
+
+class OpenAIDecisionInputText(LiteLLMPydanticObjectBase):
+ type: Literal["input_text"]
+ text: str
+
+ model_config = ConfigDict(extra="forbid", frozen=True)
+
+
+class OpenAIDecisionInputMessage(LiteLLMPydanticObjectBase):
+ role: Literal["user"] = "user"
+ type: Literal["message"] = "message"
+ content: str | Sequence[OpenAIDecisionInputText]
+
+ model_config = ConfigDict(extra="forbid", frozen=True)
+
+
+class OpenAIPredicateQuestion(LiteLLMPydanticObjectBase):
+ type: Literal["predicate"]
+ name: str | None = None
+ instructions: str
+
+ model_config = ConfigDict(extra="forbid", frozen=True)
+
+
+class OpenAIChoiceOption(LiteLLMPydanticObjectBase):
+ value: str | bool
+ description: str | None = None
+
+ model_config = ConfigDict(extra="forbid", frozen=True)
+
+
+def systemone_choice_key(value: str | bool) -> str:
+ if isinstance(value, bool):
+ return "true" if value else "false"
+ return value
+
+
+class OpenAIChoiceQuestion(LiteLLMPydanticObjectBase):
+ type: Literal["choice"]
+ name: str | None = None
+ instructions: str
+ choices: Annotated[Sequence[OpenAIChoiceOption], Field(min_length=2, max_length=255)]
+
+ model_config = ConfigDict(extra="forbid", frozen=True)
+
+ @model_validator(mode="after")
+ def require_unique_systemone_keys(self) -> "OpenAIChoiceQuestion":
+ keys: Final = frozenset(systemone_choice_key(option.value) for option in self.choices)
+ if len(keys) != len(self.choices):
+ raise ValueError("Choice values must be unique, and a boolean cannot share its text with a string choice")
+ return self
+
+
+class OpenAIScoreLevel(LiteLLMPydanticObjectBase):
+ label: str
+ description: str | None = None
+
+ model_config = ConfigDict(extra="forbid", frozen=True)
+
+
+class OpenAIScoreQuestion(LiteLLMPydanticObjectBase):
+ type: Literal["score"]
+ name: str | None = None
+ instructions: str
+ levels: Annotated[Sequence[OpenAIScoreLevel], Field(min_length=2, max_length=10)]
+
+ model_config = ConfigDict(extra="forbid", frozen=True)
+
+
+OpenAIDecisionQuestion: TypeAlias = Annotated[
+ OpenAIPredicateQuestion | OpenAIChoiceQuestion | OpenAIScoreQuestion,
+ Field(discriminator="type"),
+]
+
+
+class OpenAIDecisionRequestBody(LiteLLMPydanticObjectBase):
+ input: str | Sequence[OpenAIDecisionInputMessage]
+ questions: Annotated[Sequence[OpenAIDecisionQuestion], Field(min_length=1, max_length=MAX_DECISION_QUESTIONS)]
+ safety_identifier: str | None = None
+
+ model_config = ConfigDict(extra="allow", frozen=True)
+
+
+class OpenAIPredicateAnswer(LiteLLMPydanticObjectBase):
+ type: Literal["predicate"] = "predicate"
+ name: str | None
+ probability: float
+
+ model_config = ConfigDict(frozen=True)
+
+
+class OpenAIChoiceProbability(LiteLLMPydanticObjectBase):
+ value: str | bool
+ probability: float
+
+ model_config = ConfigDict(frozen=True)
+
+
+class OpenAIChoiceAnswer(LiteLLMPydanticObjectBase):
+ type: Literal["choice"] = "choice"
+ name: str | None
+ choice: str | bool
+ probabilities: tuple[OpenAIChoiceProbability, ...]
+ confidence: float
+
+ model_config = ConfigDict(frozen=True)
+
+
+class OpenAIScoreProbability(LiteLLMPydanticObjectBase):
+ value: int
+ label: str
+ probability: float
+
+ model_config = ConfigDict(frozen=True)
+
+
+class OpenAIScoreAnswer(LiteLLMPydanticObjectBase):
+ type: Literal["score"] = "score"
+ name: str | None
+ score: float
+ probabilities: tuple[OpenAIScoreProbability, ...]
+ confidence: float
+
+ model_config = ConfigDict(frozen=True)
+
+
+class OpenAIRefusalAnswer(LiteLLMPydanticObjectBase):
+ type: Literal["refusal"] = "refusal"
+ name: str | None
+
+ model_config = ConfigDict(frozen=True)
+
+
+OpenAIDecisionAnswer: TypeAlias = OpenAIPredicateAnswer | OpenAIChoiceAnswer | OpenAIScoreAnswer | OpenAIRefusalAnswer
+
+
+class OpenAIDecisionInputTokensDetails(LiteLLMPydanticObjectBase):
+ cached_tokens: int = 0
+ cache_write_tokens: int = 0
+
+ model_config = ConfigDict(frozen=True)
+
+
+class OpenAIDecisionOutputTokensDetails(LiteLLMPydanticObjectBase):
+ reasoning_tokens: int = 0
+
+ model_config = ConfigDict(frozen=True)
+
+
+class OpenAIDecisionUsage(LiteLLMPydanticObjectBase):
+ input_tokens: int
+ input_tokens_details: OpenAIDecisionInputTokensDetails = OpenAIDecisionInputTokensDetails()
+ output_tokens: int
+ output_tokens_details: OpenAIDecisionOutputTokensDetails = OpenAIDecisionOutputTokensDetails()
+ total_tokens: int
+
+ model_config = ConfigDict(frozen=True)
+
+
+class OpenAIDecisionResponse(LiteLLMPydanticObjectBase):
+ model: str
+ answers: tuple[OpenAIDecisionAnswer, ...]
+ usage: OpenAIDecisionUsage
+
+ model_config = ConfigDict(extra="allow", frozen=True)
diff --git a/litellm/types/utils.py b/litellm/types/utils.py
index 507bfa6c5e1..a99c22e9479 100644
--- a/litellm/types/utils.py
+++ b/litellm/types/utils.py
@@ -788,6 +788,8 @@ API_ROUTE_TO_CALL_TYPES: Final[Mapping[str, Sequence[CallTypes]]] = {
"/v1/search": [CallTypes.asearch, CallTypes.search],
"/decisions": [CallTypes.adecisions, CallTypes.decisions],
"/v1/decisions": [CallTypes.adecisions, CallTypes.decisions],
+ "/systemone": [CallTypes.adecisions, CallTypes.decisions],
+ "/v1/systemone": [CallTypes.adecisions, CallTypes.decisions],
# Batches
"/batches": [CallTypes.acreate_batch, CallTypes.create_batch],
"/v1/batches": [CallTypes.acreate_batch, CallTypes.create_batch],
diff --git a/tests/integration/cost_calculation/cost_tracking_case.py b/tests/integration/cost_calculation/cost_tracking_case.py
index 466fdc46555..8c9a53262b6 100644
--- a/tests/integration/cost_calculation/cost_tracking_case.py
+++ b/tests/integration/cost_calculation/cost_tracking_case.py
@@ -250,7 +250,7 @@ class CostTrackingTestCase(BaseModel):
"/v1/audio/speech",
"/v1/images/generations",
"/v1/images/edits",
- "/v1/decisions",
+ "/v1/systemone",
]
| Annotated[str, Field(pattern=r"^/(gemini|anthropic|bedrock)/")]
) = "/v1/chat/completions"
diff --git a/tests/integration/cost_calculation/cost_tracking_cases.json b/tests/integration/cost_calculation/cost_tracking_cases.json
index 94571f2a63c..81beff68c7c 100644
--- a/tests/integration/cost_calculation/cost_tracking_cases.json
+++ b/tests/integration/cost_calculation/cost_tracking_cases.json
@@ -31207,7 +31207,7 @@
"name": "perplexity/pplx-decider-v1-27b-decisions",
"covers": "quota_management.spend_tracking.decisions_costs",
"model": "perplexity/pplx-decider-v1-27b",
- "endpoint": "/v1/decisions",
+ "endpoint": "/v1/systemone",
"request": {
"model": "$MODEL",
"state": {
diff --git a/tests/integration/cost_calculation/test_cost_tracking.py b/tests/integration/cost_calculation/test_cost_tracking.py
index 379b2c13f5b..97dd7e494f1 100644
--- a/tests/integration/cost_calculation/test_cost_tracking.py
+++ b/tests/integration/cost_calculation/test_cost_tracking.py
@@ -268,7 +268,7 @@ def test_case_bills_expected_cost(gateway: Gateway, case: CostTrackingTestCase)
assert row.spend == 0, f"{case.name}: failure spend was {row.spend}"
return
assert response.is_success, f"{case.name}: proxy returned {response.status_code}: {response.text[:400]}"
- if case.endpoint == "/v1/decisions":
+ if case.endpoint == "/v1/systemone":
observed: Final = JSON_OBJECT.validate_json(
httpx.get(f"{gateway.upstream_url}/__observations", timeout=5, trust_env=False).content
)
diff --git a/tests/integration/providers/test_decisions_chaos.py b/tests/integration/providers/test_decisions_chaos.py
index 2e88cdbbc7f..63b4a482e92 100644
--- a/tests/integration/providers/test_decisions_chaos.py
+++ b/tests/integration/providers/test_decisions_chaos.py
@@ -27,7 +27,7 @@ _API_KEY: Final = "synthetic-decisions-key"
_JSON_OBJECT: Final = TypeAdapter(dict[str, JsonValue])
_STARTED_WORKER: Final = re.compile(r"Started server process \[(\d+)\]")
_QUESTIONS: Final[dict[str, JsonValue]] = {"fine": {"type": "noul", "instructions": "Is the state fine?"}}
-_ROUTES: Final = ("/v1/decisions", "/decisions")
+_ROUTES: Final = ("/v1/systemone", "/systemone")
@dataclass(frozen=True, slots=True)
@@ -221,7 +221,7 @@ async def test_worker_sigkill_mid_burst_leaves_the_sibling_serving_the_default_m
release.set()
served: Final = await burst
assert len(served) == held_by[survivor_pid], (held_by, len(served))
- follow_up: Final = _Call(route="/decisions", marker=f"ok-{uuid.uuid4().hex}", fail=False)
+ follow_up: Final = _Call(route="/systemone", marker=f"ok-{uuid.uuid4().hex}", fail=False)
(answered,) = await _burst(base_url, candidate.key, None, (follow_up,))
await asyncio.to_thread(
eventually,
diff --git a/tests/integration/providers/test_decisions_wire.py b/tests/integration/providers/test_decisions_wire.py
index 548a0d27f89..84b31088d60 100644
--- a/tests/integration/providers/test_decisions_wire.py
+++ b/tests/integration/providers/test_decisions_wire.py
@@ -158,7 +158,7 @@ def _deployment(scenario: Scenario, handle: ScenarioHandle, provider: _Provider)
def _decide(gateway: Gateway, model: str, *, key: str | None = None, **extra: JsonValue) -> httpx.Response:
return gateway.request(
- "POST", "/v1/decisions", {"model": model, "state": _STATE, "questions": _QUESTIONS, **extra}, key=key
+ "POST", "/v1/systemone", {"model": model, "state": _STATE, "questions": _QUESTIONS, **extra}, key=key
)
@@ -310,7 +310,7 @@ def test_invalid_bodies_are_refused_at_the_gateway_without_an_upstream_call(gate
handle: Final = _register(scenario, _answer_body(_PERPLEXITY))
model: Final = _deployment(scenario, handle, _PERPLEXITY)
for label, body in _INVALID_BODIES:
- response: Final = gateway.request("POST", "/v1/decisions", {"model": model, **body})
+ response: Final = gateway.request("POST", "/v1/systemone", {"model": model, **body})
assert response.status_code == 400, (label, response.text)
assert "Invalid Decisions request" in response.text, (label, response.text)
assert _upstream_calls(gateway, handle) == []
@@ -329,7 +329,7 @@ def test_key_checks_match_chat(gateway: Gateway) -> None:
handle: Final = _register(scenario, _answer_body(_PERPLEXITY))
model: Final = _deployment(scenario, handle, _PERPLEXITY)
anonymous: Final = gateway.client.post(
- "/v1/decisions", json={"model": model, "state": _STATE, "questions": _QUESTIONS}
+ "/v1/systemone", json={"model": model, "state": _STATE, "questions": _QUESTIONS}
)
assert anonymous.status_code == 401, anonymous.text
restricted: Final = scenario.key(models=[f"other-{uuid.uuid4().hex}"])
@@ -395,7 +395,7 @@ def test_a_deployment_opted_into_client_api_base_sends_decisions_and_chat_to_the
assert _calls_to(observed, configured) == []
-def test_a_config_pass_through_at_v1_decisions_keeps_answering_and_the_native_api_serves_decisions(
+def test_a_config_pass_through_at_v1_decisions_keeps_answering_and_the_native_api_serves_system_one(
gateway: Gateway, tmp_path: Path
) -> None:
with gateway.scenario() as scenario:
@@ -407,9 +407,11 @@ def test_a_config_pass_through_at_v1_decisions_keeps_answering_and_the_native_ap
tmp_path, f"{pass_through_target.api_base()}/v1/decisions", native_target.api_base()
)
with owned_proxy_process(gateway, tmp_path, {}, config=config) as owned:
- through: Final = _decide(owned.gateway, _PASS_THROUGH_MODEL)
+ through: Final = owned.gateway.request(
+ "POST", "/v1/decisions", {"model": _PASS_THROUGH_MODEL, "state": _STATE, "questions": _QUESTIONS}
+ )
native: Final = owned.gateway.request(
- "POST", "/decisions", {"model": _PASS_THROUGH_NEIGHBOUR, "state": _STATE, "questions": _QUESTIONS}
+ "POST", "/systemone", {"model": _PASS_THROUGH_NEIGHBOUR, "state": _STATE, "questions": _QUESTIONS}
)
assert through.status_code == 200, through.text
assert through.json() == {"model": _PASS_THROUGH_MODEL, "answers": _ANSWERS, "usage": _USAGE}
diff --git a/tests/unit/decisions/test_openai_transformation.py b/tests/unit/decisions/test_openai_transformation.py
new file mode 100644
index 00000000000..a3cc1583d40
--- /dev/null
+++ b/tests/unit/decisions/test_openai_transformation.py
@@ -0,0 +1,32 @@
+from collections.abc import Mapping
+from typing import Final
+
+import pytest
+from pydantic import TypeAdapter, ValidationError
+
+from litellm.decisions.openai_transformation import to_systemone_request
+from litellm.types.decisions import MAX_DECISION_QUESTIONS, DecisionsRequestBody, OpenAIDecisionRequestBody
+
+_OPENAI_BODY: Final[TypeAdapter[OpenAIDecisionRequestBody]] = TypeAdapter(OpenAIDecisionRequestBody)
+_SYSTEMONE_BODY: Final[TypeAdapter[DecisionsRequestBody]] = TypeAdapter(DecisionsRequestBody)
+
+
+def _openai_request(question_count: int) -> Mapping[str, object]:
+ return {
+ "model": "decider",
+ "input": "The package arrived with a broken screen.",
+ "questions": [{"type": "predicate", "instructions": f"Question {index}?"} for index in range(question_count)],
+ }
+
+
+def test_the_largest_openai_request_accepted_translates_to_a_valid_systemone_request() -> None:
+ raw: Final = _openai_request(MAX_DECISION_QUESTIONS)
+
+ translated: Final = _SYSTEMONE_BODY.validate_python(to_systemone_request(raw, _OPENAI_BODY.validate_python(raw)))
+
+ assert len(translated.questions) == MAX_DECISION_QUESTIONS
+
+
+def test_an_openai_request_with_more_questions_than_systemone_takes_is_rejected_before_translation() -> None:
+ with pytest.raises(ValidationError, match="questions"):
+ _OPENAI_BODY.validate_python(_openai_request(MAX_DECISION_QUESTIONS + 1))
diff --git a/tests/unit/proxy/decisions_endpoints/test_endpoints.py b/tests/unit/proxy/decisions_endpoints/test_endpoints.py
index 6b1ac9e3404..b00f2be092c 100644
--- a/tests/unit/proxy/decisions_endpoints/test_endpoints.py
+++ b/tests/unit/proxy/decisions_endpoints/test_endpoints.py
@@ -16,7 +16,7 @@ from starlette.routing import Match
import litellm
from litellm.proxy._lazy_features import LAZY_FEATURES, LazyFeature, attach_lazy_features
-from litellm.proxy.decisions_endpoints.endpoints import decisions
+from litellm.proxy.decisions_endpoints.endpoints import decisions, systemone
from litellm.proxy.pass_through_endpoints.pass_through_endpoints import SafeRouteAdder
from litellm.proxy.proxy_server import (
app,
@@ -77,7 +77,7 @@ def client(monkeypatch: pytest.MonkeyPatch) -> Iterator[TestClient]:
litellm.in_memory_llm_clients_cache.flush_cache()
-@pytest.mark.parametrize("endpoint", ("/v1/decisions", "/decisions"))
+@pytest.mark.parametrize("endpoint", ("/v1/systemone", "/systemone"))
def test_proxy_decisions_route_returns_answers_and_cost(
client: TestClient,
respx_mock: respx.MockRouter,
@@ -126,7 +126,7 @@ def test_proxy_decisions_dispatches_typesafe_deployment(
upstream: Final = respx_mock.post("https://api.typesafe.ai/v1/systemone").respond(json=_RESPONSE)
response: Final = client.post(
- "/v1/decisions",
+ "/v1/systemone",
json={
"model": "jev",
"state": {"source": "proxy-test"},
@@ -165,7 +165,7 @@ def test_proxy_decisions_sends_the_env_key_to_the_deployment_api_base(
monkeypatch.setattr(litellm.proxy.proxy_server, "llm_router", router)
upstream: Final = respx_mock.post("https://egress.example/perplexity/v1/decisions").respond(json=_RESPONSE)
- response: Final = client.post("/v1/decisions", json=_REQUEST)
+ response: Final = client.post("/v1/systemone", json=_REQUEST)
assert response.status_code == 200, response.text
assert upstream.call_count == 1
@@ -177,7 +177,7 @@ def test_proxy_decisions_unknown_model_is_a_client_error(
respx_mock: respx.MockRouter,
) -> None:
response: Final = client.post(
- "/v1/decisions",
+ "/v1/systemone",
json={
"model": "missing-model",
"state": "review",
@@ -210,7 +210,7 @@ def test_proxy_decisions_missing_required_field_is_a_client_error(
) -> None:
upstream: Final = respx_mock.post("https://api.perplexity.ai/v1/decisions").respond(json=_RESPONSE)
- response: Final = client.post("/v1/decisions", json=request_body)
+ response: Final = client.post("/v1/systemone", json=request_body)
assert response.status_code == 400, response.text
assert not upstream.called
@@ -238,7 +238,7 @@ def test_proxy_decisions_dispatches_strands_decider(
upstream: Final = respx_mock.post("https://strands.example/v1/systemone").respond(json=_STRANDS_RESPONSE)
response: Final = client.post(
- "/v1/decisions",
+ "/v1/systemone",
json={
"model": "strands",
"state": {"source": "proxy-test"},
@@ -266,7 +266,7 @@ def test_proxy_decisions_without_model_uses_the_proxy_default_model(
upstream: Final = respx_mock.post("https://api.perplexity.ai/v1/decisions").respond(json=_RESPONSE)
response: Final = client.post(
- "/v1/decisions", json={key: value for key, value in _REQUEST.items() if key != "model"}
+ "/v1/systemone", json={key: value for key, value in _REQUEST.items() if key != "model"}
)
assert response.status_code == 200, response.text
@@ -275,6 +275,193 @@ def test_proxy_decisions_without_model_uses_the_proxy_default_model(
assert json.loads(upstream.calls[0].request.content)["model"] == "pplx-decider-v1-27b"
+_OPENAI_FORMAT_REQUEST: Final[Mapping[str, object]] = {
+ "model": "decider",
+ "input": [
+ {
+ "role": "user",
+ "content": [
+ {"type": "input_text", "text": "The package arrived with a broken screen."},
+ {"type": "input_text", "text": "I want a refund."},
+ ],
+ },
+ {"role": "user", "content": "Order 1234."},
+ ],
+ "questions": [
+ {"type": "predicate", "name": "damaged", "instructions": "Does the customer report a damaged item?"},
+ {
+ "type": "choice",
+ "instructions": "Should we refund?",
+ "choices": [{"value": True, "description": "Refund now"}, {"value": "escalate"}],
+ },
+ {
+ "type": "score",
+ "name": "severity",
+ "instructions": "How severe is the issue?",
+ "levels": [{"label": "minor"}, {"label": "major", "description": "Product unusable"}],
+ },
+ {"type": "predicate", "name": "fraud", "instructions": "Is this fraud?"},
+ ],
+ "safety_identifier": "end-user-1",
+}
+_SYSTEMONE_ANSWERS_FOR_OPENAI_REQUEST: Final[Mapping[str, object]] = {
+ "model": "pplx-decider-v1-27b",
+ "answers": {
+ "0": {"type": "noul", "noul": 0.95},
+ "1": {"type": "choice", "choice": "true", "confidence": 0.8, "probabilities": {"true": 0.9, "escalate": 0.1}},
+ "2": {
+ "type": "score",
+ "score": 0.7,
+ "confidence": 0.6,
+ "legend": {"0": "minor", "1": "major: Product unusable"},
+ "probabilities": {"0": 0.3, "1": 0.7},
+ },
+ },
+ "usage": {"input_tokens": _INPUT_TOKENS, "output_tokens": _OUTPUT_TOKENS},
+}
+
+
+@pytest.mark.parametrize("endpoint", ("/v1/decisions", "/decisions"))
+def test_openai_format_decisions_translate_through_systemone(
+ client: TestClient,
+ respx_mock: respx.MockRouter,
+ endpoint: str,
+) -> None:
+ upstream: Final = respx_mock.post("https://api.perplexity.ai/v1/decisions").respond(
+ json=_SYSTEMONE_ANSWERS_FOR_OPENAI_REQUEST
+ )
+
+ response: Final = client.post(endpoint, json=_OPENAI_FORMAT_REQUEST)
+
+ assert response.status_code == 200, response.text
+ assert json.loads(upstream.calls[0].request.content) == {
+ "model": "pplx-decider-v1-27b",
+ "state": "The package arrived with a broken screen.\n\nI want a refund.\n\nOrder 1234.",
+ "questions": {
+ "0": {"type": "noul", "instructions": "Does the customer report a damaged item?"},
+ "1": {
+ "type": "choice",
+ "instructions": "Should we refund?",
+ "criteria": {"true": "Refund now", "escalate": None},
+ },
+ "2": {
+ "type": "score",
+ "instructions": "How severe is the issue?",
+ "criteria": ["minor", "major: Product unusable"],
+ },
+ "3": {"type": "noul", "instructions": "Is this fraud?"},
+ },
+ }
+ body: Final = response.json()
+ assert body["model"] == _SYSTEMONE_ANSWERS_FOR_OPENAI_REQUEST["model"]
+ assert body["answers"] == [
+ {"type": "predicate", "name": "damaged", "probability": 0.95},
+ {
+ "type": "choice",
+ "name": None,
+ "choice": True,
+ "probabilities": [{"value": True, "probability": 0.9}, {"value": "escalate", "probability": 0.1}],
+ "confidence": 0.8,
+ },
+ {
+ "type": "score",
+ "name": "severity",
+ "score": 0.7,
+ "probabilities": [
+ {"value": 0, "label": "minor", "probability": 0.3},
+ {"value": 1, "label": "major", "probability": 0.7},
+ ],
+ "confidence": 0.6,
+ },
+ {"type": "refusal", "name": "fraud"},
+ ]
+ assert body["usage"] == {
+ "input_tokens": _INPUT_TOKENS,
+ "input_tokens_details": {"cached_tokens": 0, "cache_write_tokens": 0},
+ "output_tokens": _OUTPUT_TOKENS,
+ "output_tokens_details": {"reasoning_tokens": 0},
+ "total_tokens": _INPUT_TOKENS + _OUTPUT_TOKENS,
+ }
+ assert float(response.headers["x-litellm-response-cost"]) > 0
+
+
+@pytest.mark.parametrize(
+ ("endpoint", "request_body", "upstream_response"),
+ (
+ ("/v1/systemone", _REQUEST, _RESPONSE),
+ ("/v1/decisions", _OPENAI_FORMAT_REQUEST, _SYSTEMONE_ANSWERS_FOR_OPENAI_REQUEST),
+ ),
+ ids=("systemone", "openai_format"),
+)
+def test_decisions_return_guardrail_information_when_requested(
+ client: TestClient,
+ respx_mock: respx.MockRouter,
+ endpoint: str,
+ request_body: Mapping[str, object],
+ upstream_response: Mapping[str, object],
+) -> None:
+ respx_mock.post("https://api.perplexity.ai/v1/decisions").respond(json=upstream_response)
+
+ response: Final = client.post(endpoint, json={**request_body, "include_guardrail_response": True})
+
+ assert response.status_code == 200, response.text
+ assert response.json()["guardrail_information"] == []
+
+
+@pytest.mark.parametrize(
+ "request_body",
+ (
+ _REQUEST,
+ {
+ "model": "decider",
+ "input": "review",
+ "questions": [
+ {
+ "type": "choice",
+ "instructions": "Pick one",
+ "choices": [{"value": True}, {"value": "true"}],
+ }
+ ],
+ },
+ {
+ "model": "decider",
+ "input": [
+ {"role": "user", "content": [{"type": "input_image", "image_url": "data:image/png;base64,AA=="}]}
+ ],
+ "questions": [{"type": "predicate", "instructions": "Is this a defect?"}],
+ },
+ ),
+ ids=("systemone_body", "colliding_choice_values", "image_input"),
+)
+def test_openai_format_decisions_rejects_bodies_it_cannot_translate(
+ client: TestClient,
+ respx_mock: respx.MockRouter,
+ request_body: Mapping[str, object],
+) -> None:
+ upstream: Final = respx_mock.post("https://api.perplexity.ai/v1/decisions").respond(json=_RESPONSE)
+
+ response: Final = client.post("/v1/decisions", json=request_body)
+
+ assert response.status_code == 400, response.text
+ assert not upstream.called
+
+
+@pytest.mark.parametrize("endpoint", ("/v1/systemone", "/v1/decisions"))
+@pytest.mark.parametrize("raw_body", (b"", b"{not json"), ids=("empty", "malformed"))
+def test_a_body_that_is_not_json_is_a_client_error(
+ client: TestClient,
+ respx_mock: respx.MockRouter,
+ endpoint: str,
+ raw_body: bytes,
+) -> None:
+ upstream: Final = respx_mock.post("https://api.perplexity.ai/v1/decisions").respond(json=_RESPONSE)
+
+ response: Final = client.post(endpoint, content=raw_body, headers={"Content-Type": "application/json"})
+
+ assert response.status_code == 400, response.text
+ assert not upstream.called
+
+
def _decisions_feature() -> LazyFeature:
return next(feature for feature in LAZY_FEATURES if feature.name == "decisions")
@@ -301,6 +488,8 @@ def test_a_config_pass_through_at_v1_decisions_keeps_its_route_and_the_native_ap
assert client.post("/v1/decisions", json={"model": "gpt-6-luna"}).json() == {"served_by": "pass-through"}
assert _serving_endpoint(bare, "/v1/decisions") is pass_through
assert _serving_endpoint(bare, "/decisions") is decisions
+ assert _serving_endpoint(bare, "/v1/systemone") is systemone
+ assert _serving_endpoint(bare, "/systemone") is systemone
def test_with_lazy_routes_disabled_a_config_pass_through_at_v1_decisions_still_wins(
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/SystemOneUI.integration.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/SystemOneUI.integration.test.tsx
index 2cc7e704499..0812e40d3e4 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/SystemOneUI.integration.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/SystemOneUI.integration.test.tsx
@@ -75,7 +75,7 @@ describe("SystemOneUI integration", () => {
render();
screen.getByRole("combobox", { name: "Decision endpoint" }).focus();
await user.keyboard("{ArrowDown}");
- await user.click(await screen.findByRole("option", { name: "Decisions · /v1/decisions" }));
+ await user.click(await screen.findByRole("option", { name: "System One · /v1/systemone" }));
expect(screen.getByRole("note", { name: "Decision endpoint notice" })).toHaveTextContent(
"omit model to use the proxy's configured default.",
@@ -162,7 +162,7 @@ describe("SystemOneUI integration", () => {
render();
screen.getByRole("combobox", { name: "Decision endpoint" }).focus();
await user.keyboard("{ArrowDown}");
- await user.click(await screen.findByRole("option", { name: "Decisions · /v1/decisions" }));
+ await user.click(await screen.findByRole("option", { name: "System One · /v1/systemone" }));
const editor = screen.getByRole("textbox", { name: "System One JSON payload" });
const draft = JSON.stringify({
model: "my-decider",
@@ -185,12 +185,12 @@ describe("SystemOneUI integration", () => {
screen.getByRole("combobox", { name: "Decision endpoint" }).focus();
await user.keyboard("{ArrowDown}");
- await user.click(await screen.findByRole("option", { name: "Decisions · /v1/decisions" }));
+ await user.click(await screen.findByRole("option", { name: "System One · /v1/systemone" }));
expect(editor).toHaveValue(draft);
expect(screen.queryByText("Selected choice")).not.toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Send" }));
expect(await screen.findByText("Selected choice")).toBeInTheDocument();
- expect(mockFetch.mock.calls[1]?.[0]).toMatch(/\/v1\/decisions$/);
+ expect(mockFetch.mock.calls[1]?.[0]).toMatch(/\/v1\/systemone$/);
expect(JSON.parse(mockFetch.mock.calls[1]?.[1]?.body as string)).toEqual(JSON.parse(draft));
});
@@ -199,7 +199,7 @@ describe("SystemOneUI integration", () => {
render();
screen.getByRole("combobox", { name: "Decision endpoint" }).focus();
await user.keyboard("{ArrowDown}");
- await user.click(await screen.findByRole("option", { name: "Decisions · /v1/decisions" }));
+ await user.click(await screen.findByRole("option", { name: "System One · /v1/systemone" }));
const payload = { state: "An outage", questions: { urgent: { type: "noul", instructions: "Is this urgent?" } } };
fireEvent.change(screen.getByRole("textbox", { name: "System One JSON payload" }), {
target: { value: JSON.stringify(payload) },
@@ -207,7 +207,7 @@ describe("SystemOneUI integration", () => {
expect(screen.getByRole("button", { name: "Send" })).toBeEnabled();
await user.click(screen.getByRole("button", { name: "Send" }));
expect(await screen.findByText("jev-1.13.0")).toBeInTheDocument();
- expect(mockFetch.mock.calls[0]?.[0]).toMatch(/\/v1\/decisions$/);
+ expect(mockFetch.mock.calls[0]?.[0]).toMatch(/\/v1\/systemone$/);
const body = JSON.parse(mockFetch.mock.calls[0]?.[1]?.body as string);
expect(body).toEqual(payload);
expect(body).not.toHaveProperty("model");
@@ -227,7 +227,7 @@ describe("SystemOneUI integration", () => {
await screen.findByRole("button", { name: "Cancel request" });
screen.getByRole("combobox", { name: "Decision endpoint" }).focus();
await user.keyboard("{ArrowDown}");
- await user.click(await screen.findByRole("option", { name: "Decisions · /v1/decisions" }));
+ await user.click(await screen.findByRole("option", { name: "System One · /v1/systemone" }));
expect(mockFetch.mock.calls[0]?.[1]?.signal?.aborted).toBe(true);
expect(screen.getByRole("button", { name: "Send" })).toBeEnabled();
@@ -249,7 +249,7 @@ describe("SystemOneUI integration", () => {
await user.click(screen.getByRole("button", { name: "Send" }));
expect(await screen.findByText("jev-1.13.0")).toBeInTheDocument();
expect(screen.getByText("Selected choice")).toBeInTheDocument();
- expect(mockFetch.mock.calls[1]?.[0]).toMatch(/\/v1\/decisions$/);
+ expect(mockFetch.mock.calls[1]?.[0]).toMatch(/\/v1\/systemone$/);
expect(mockFetch).toHaveBeenCalledTimes(2);
});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/SystemOneUI.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/SystemOneUI.tsx
index 7b312eab081..e092efce161 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/SystemOneUI.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/SystemOneUI.tsx
@@ -42,11 +42,11 @@ export default function SystemOneUI({ accessToken, disabledPersonalKeyCreation =
const [customApiKey, setCustomApiKey] = useState("");
const [endpoint, setEndpoint] = useState("/typesafe/v1/systemone");
const [payloads, setPayloads] = useState>({
- "/v1/decisions": DECISIONS_EXAMPLE_PAYLOAD,
+ "/v1/systemone": DECISIONS_EXAMPLE_PAYLOAD,
"/typesafe/v1/systemone": EXAMPLE_PAYLOAD,
});
const rawPayload = payloads[endpoint];
- const examplePayload = endpoint === "/v1/decisions" ? DECISIONS_EXAMPLE_PAYLOAD : EXAMPLE_PAYLOAD;
+ const examplePayload = endpoint === "/v1/systemone" ? DECISIONS_EXAMPLE_PAYLOAD : EXAMPLE_PAYLOAD;
const activeController = useRef(null);
const validation = useMemo(() => validateSystemOnePayload(rawPayload, endpoint), [rawPayload, endpoint]);
const effectiveApiKey = apiKeySource === "session" ? accessToken || "" : customApiKey.trim();
@@ -105,7 +105,7 @@ export default function SystemOneUI({ accessToken, disabledPersonalKeyCreation =
@@ -182,11 +182,11 @@ export default function SystemOneUI({ accessToken, disabledPersonalKeyCreation =
- {endpoint === "/v1/decisions" ? "Decision models · Jev format" : "TypeSafe Jev · System One"}
+ {endpoint === "/v1/systemone" ? "Decision models · System One" : "TypeSafe Jev · System One"}
- {endpoint === "/v1/decisions"
- ? "Sends choice, noul, and score questions through /v1/decisions. Replace the example model with a decision model configured on your proxy, or omit model to use the proxy's configured default."
+ {endpoint === "/v1/systemone"
+ ? "Sends choice, noul, and score questions through /v1/systemone. Replace the example model with a decision model configured on your proxy, or omit model to use the proxy's configured default."
: "Sends requests through /typesafe/v1/systemone and requires TYPESAFE_API_KEY on the proxy."}{" "}
Give us feedback on what you want for decision models
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/decisions.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/decisions.test.ts
index 1ae9b2a130f..90f1b63b09f 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/decisions.test.ts
+++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/decisions.test.ts
@@ -12,7 +12,7 @@ const request = {
},
provider_option: { enabled: true },
};
-const validate = (value: unknown) => validateSystemOnePayload(JSON.stringify(value), "/v1/decisions");
+const validate = (value: unknown) => validateSystemOnePayload(JSON.stringify(value), "/v1/systemone");
describe("native decisions validation", () => {
it("accepts structured Jev criteria, optional instructions, and provider extensions without dropping fields", () => {
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/schemas.ts b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/schemas.ts
index 585a2a36921..9da040256bc 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/schemas.ts
+++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/schemas.ts
@@ -1,6 +1,6 @@
import { z } from "zod";
-export type DecisionEndpoint = "/v1/decisions" | "/typesafe/v1/systemone";
+export type DecisionEndpoint = "/v1/systemone" | "/typesafe/v1/systemone";
const decisionsJson = z.union([z.string(), z.record(z.string(), z.unknown()), z.array(z.unknown())]);
const decisionInstructions = decisionsJson.nullish();
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/validatePayload.ts b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/validatePayload.ts
index eac96e67045..ac53f871333 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/validatePayload.ts
+++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/validatePayload.ts
@@ -54,7 +54,7 @@ export function validateSystemOnePayload(
return invalid("syntax", `Invalid JSON syntax: ${json.message}`);
}
- const schema = endpoint === "/v1/decisions" ? decisionsRequestSchema : systemOneRequestSchema;
+ const schema = endpoint === "/v1/systemone" ? decisionsRequestSchema : systemOneRequestSchema;
const result = schema.safeParse(json.value);
if (!result.success) {
return {
diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts
index 54de1d30d3e..1108d4a8743 100644
--- a/ui/litellm-dashboard/src/lib/http/schema.d.ts
+++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts
@@ -16321,6 +16321,23 @@ export interface paths {
patch?: never;
trace?: never;
};
+ "/systemone": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** Systemone */
+ post: operations["systemone_systemone_post"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/tag/daily/activity": {
parameters: {
query?: never;
@@ -22434,6 +22451,23 @@ export interface paths {
patch?: never;
trace?: never;
};
+ "/v1/systemone": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** Systemone */
+ post: operations["systemone_v1_systemone_post"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/v1/threads": {
parameters: {
query?: never;
@@ -73321,6 +73355,26 @@ export interface operations {
};
};
};
+ systemone_systemone_post: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": unknown;
+ };
+ };
+ };
+ };
get_tag_daily_activity_tag_daily_activity_get: {
parameters: {
query?: {
@@ -81595,6 +81649,26 @@ export interface operations {
};
};
};
+ systemone_v1_systemone_post: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": unknown;
+ };
+ };
+ };
+ };
create_threads_v1_threads_post: {
parameters: {
query?: never;